v1.0.0-dev: slim to the real product — merchant state machine only
This commit is contained in:
parent
c31518309d
commit
b4a81e0ab8
15 changed files with 286 additions and 2503 deletions
|
|
@ -1,71 +0,0 @@
|
|||
"""Deterministic address-derivation smoke test.
|
||||
|
||||
Uses a known test-vector xpub (the one shipped in the pycardano docs)
|
||||
to assert the derived addresses are stable and reproducible across SDK
|
||||
versions. If this test ever changes output, we have a backwards-compat
|
||||
problem that would break every merchant's receive-address history.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from cardano_checkout import addresses
|
||||
|
||||
|
||||
# Public test vector — a CIP-1852 account extended public key.
|
||||
# 64 bytes = 32 bytes Ed25519 pubkey || 32 bytes chain code, hex encoded.
|
||||
#
|
||||
# Derived deterministically from the well-known test mnemonic
|
||||
# "test test test test test test test test test test test junk"
|
||||
# at path m/1852'/1815'/0' via pycardano's HDWallet. Using a real,
|
||||
# on-curve account xpub here (as opposed to random hex) is what lets
|
||||
# validate_xpub + derive_address actually exercise the BIP32 math.
|
||||
TEST_XPUB_HEX = (
|
||||
"f2cdeef60dfc2c00cd1d4c0def0ce3f7b0328f5badd2fd771f48ff207ca7eaa8"
|
||||
"500a3c3d556f995e79c4a75e64d13ab12772f46e6c05fed1d9698b7e12a533f7"
|
||||
)
|
||||
|
||||
|
||||
def test_validate_xpub_accepts_well_formed_key() -> None:
|
||||
assert addresses.validate_xpub(TEST_XPUB_HEX) is True
|
||||
|
||||
|
||||
def test_validate_xpub_rejects_empty_and_junk() -> None:
|
||||
assert addresses.validate_xpub("") is False
|
||||
assert addresses.validate_xpub("notreallyhex!!") is False
|
||||
assert addresses.validate_xpub("deadbeef") is False # wrong length
|
||||
# Note: a correct-length random-hex string IS accepted — BIP32-ED25519
|
||||
# soft derivation over a 64-byte input doesn't require the public key
|
||||
# half to be a point on the curve. We only catch shape errors here.
|
||||
|
||||
|
||||
def test_derive_address_is_deterministic() -> None:
|
||||
a0 = addresses.derive_address(TEST_XPUB_HEX, index=0, network="mainnet")
|
||||
a0_again = addresses.derive_address(TEST_XPUB_HEX, index=0, network="mainnet")
|
||||
assert a0 == a0_again
|
||||
assert a0.startswith("addr1")
|
||||
|
||||
|
||||
def test_derive_address_distinct_per_index() -> None:
|
||||
a0 = addresses.derive_address(TEST_XPUB_HEX, index=0, network="mainnet")
|
||||
a1 = addresses.derive_address(TEST_XPUB_HEX, index=1, network="mainnet")
|
||||
a42 = addresses.derive_address(TEST_XPUB_HEX, index=42, network="mainnet")
|
||||
assert a0 != a1 != a42
|
||||
|
||||
|
||||
def test_derive_address_network_switch_changes_prefix() -> None:
|
||||
mainnet = addresses.derive_address(TEST_XPUB_HEX, index=0, network="mainnet")
|
||||
testnet = addresses.derive_address(TEST_XPUB_HEX, index=0, network="testnet")
|
||||
assert mainnet.startswith("addr1")
|
||||
assert testnet.startswith("addr_test1")
|
||||
|
||||
|
||||
def test_derive_address_rejects_negative_index() -> None:
|
||||
with pytest.raises(ValueError, match="non-negative"):
|
||||
addresses.derive_address(TEST_XPUB_HEX, index=-1)
|
||||
|
||||
|
||||
def test_derive_address_rejects_bad_network() -> None:
|
||||
with pytest.raises(ValueError, match="Invalid network"):
|
||||
addresses.derive_address(TEST_XPUB_HEX, index=0, network="preprod")
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
"""CIP-25 v2 metadata envelope construction — pure unit tests, no network."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from cardano_checkout.mint import build_cip25_metadata
|
||||
|
||||
|
||||
def test_basic_envelope_shape() -> None:
|
||||
md = build_cip25_metadata(
|
||||
policy_id="abc123",
|
||||
asset_name="ExampleStudio-Order-0001",
|
||||
name="Example Studio — Custom Order #0001",
|
||||
image_cid="bafybeibgen",
|
||||
description="Hand-stitched moth pendant",
|
||||
properties={"order_id": "0001", "edition": "1 of 1"},
|
||||
)
|
||||
|
||||
assert md["721"]["version"] == "2.0"
|
||||
assert "abc123" in md["721"]
|
||||
|
||||
nft = md["721"]["abc123"]["ExampleStudio-Order-0001"]
|
||||
assert nft["name"] == "Example Studio — Custom Order #0001"
|
||||
assert nft["image"] == "ipfs://bafybeibgen"
|
||||
assert nft["mediaType"] == "image/jpeg"
|
||||
assert nft["description"] == "Hand-stitched moth pendant"
|
||||
assert nft["order_id"] == "0001"
|
||||
assert nft["edition"] == "1 of 1"
|
||||
|
||||
|
||||
def test_description_under_64_chars_stays_a_string() -> None:
|
||||
md = build_cip25_metadata(
|
||||
policy_id="abc", asset_name="x", name="n",
|
||||
image_cid="c", description="short",
|
||||
)
|
||||
assert md["721"]["abc"]["x"]["description"] == "short"
|
||||
|
||||
|
||||
def test_description_over_64_chars_chunks_to_list() -> None:
|
||||
long = "x" * 150
|
||||
md = build_cip25_metadata(
|
||||
policy_id="abc", asset_name="x", name="n",
|
||||
image_cid="c", description=long,
|
||||
)
|
||||
desc = md["721"]["abc"]["x"]["description"]
|
||||
assert isinstance(desc, list)
|
||||
assert all(len(chunk) <= 64 for chunk in desc)
|
||||
assert "".join(desc) == long
|
||||
|
||||
|
||||
def test_image_uri_has_ipfs_prefix() -> None:
|
||||
md = build_cip25_metadata(
|
||||
policy_id="abc", asset_name="x", name="n",
|
||||
image_cid="bafybeitestcid",
|
||||
)
|
||||
assert md["721"]["abc"]["x"]["image"].startswith("ipfs://")
|
||||
assert "bafybeitestcid" in md["721"]["abc"]["x"]["image"]
|
||||
|
|
@ -1,308 +0,0 @@
|
|||
"""CIP-25 v2 envelope round-trips + unsigned-mint shape tests.
|
||||
|
||||
Complements ``test_cip25_metadata.py`` (the pure builder unit tests) by
|
||||
checking:
|
||||
|
||||
- Round-tripping the envelope through pycardano's Metadata/AuxiliaryData
|
||||
produces a well-formed CBOR blob (the wallet-visible thing).
|
||||
- :func:`mint_nft_cert` returns a correctly-shaped :class:`UnsignedMint`
|
||||
without hitting a live chain — we stub the ChainContext.
|
||||
|
||||
The chain-context stub mirrors just enough of pycardano's interface for
|
||||
``TransactionBuilder.build`` to succeed. No live Ogmios calls.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from cardano_checkout import UnsignedMint, build_cip25_metadata, mint_nft_cert
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures — a deterministic test policy + a stub ChainContext
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _test_policy():
|
||||
"""Return a fresh 2-of-2 NativeScript all-of policy + its hash + CBOR.
|
||||
|
||||
Uses fixed verification-key hashes (32-char hex, 28 bytes as
|
||||
required by Cardano VKH). No cryptographic significance — just
|
||||
deterministic filler for tests.
|
||||
"""
|
||||
from pycardano import (
|
||||
NativeScript,
|
||||
ScriptAll,
|
||||
ScriptPubkey,
|
||||
VerificationKeyHash,
|
||||
)
|
||||
|
||||
vkh_signer1 = VerificationKeyHash.from_primitive(bytes.fromhex("11" * 28))
|
||||
vkh_signer2 = VerificationKeyHash.from_primitive(bytes.fromhex("22" * 28))
|
||||
script: NativeScript = ScriptAll(
|
||||
[ScriptPubkey(vkh_signer1), ScriptPubkey(vkh_signer2)]
|
||||
)
|
||||
return script, [vkh_signer1.payload.hex(), vkh_signer2.payload.hex()]
|
||||
|
||||
|
||||
def _stub_context():
|
||||
"""Return a minimal ChainContext stub with just enough surface for builder.build()."""
|
||||
from pycardano import (
|
||||
Address,
|
||||
AssetName,
|
||||
MultiAsset,
|
||||
ProtocolParameters,
|
||||
TransactionId,
|
||||
TransactionInput,
|
||||
TransactionOutput,
|
||||
UTxO,
|
||||
Value,
|
||||
)
|
||||
|
||||
class StubContext:
|
||||
network = None
|
||||
|
||||
@property
|
||||
def last_block_slot(self) -> int:
|
||||
return 100_000_000
|
||||
|
||||
@property
|
||||
def protocol_param(self) -> ProtocolParameters:
|
||||
# Mainnet values as of 2025-ish — just enough to get fee math through.
|
||||
return ProtocolParameters(
|
||||
min_fee_constant=155_381,
|
||||
min_fee_coefficient=44,
|
||||
max_block_size=90_112,
|
||||
max_tx_size=16_384,
|
||||
max_block_header_size=1_100,
|
||||
key_deposit=2_000_000,
|
||||
pool_deposit=500_000_000,
|
||||
pool_influence=0.3,
|
||||
monetary_expansion=0.003,
|
||||
treasury_expansion=0.2,
|
||||
decentralization_param=0,
|
||||
extra_entropy="",
|
||||
protocol_major_version=9,
|
||||
protocol_minor_version=0,
|
||||
min_utxo=1_000_000,
|
||||
min_pool_cost=340_000_000,
|
||||
price_mem=0.0577,
|
||||
price_step=0.0000721,
|
||||
max_tx_ex_mem=14_000_000,
|
||||
max_tx_ex_steps=10_000_000_000,
|
||||
max_block_ex_mem=62_000_000,
|
||||
max_block_ex_steps=20_000_000_000,
|
||||
max_val_size=5_000,
|
||||
collateral_percent=150,
|
||||
max_collateral_inputs=3,
|
||||
coins_per_utxo_byte=4_310,
|
||||
coins_per_utxo_word=34_482,
|
||||
cost_models={}, # Plutus cost models — not used by native-script mints.
|
||||
)
|
||||
|
||||
@property
|
||||
def genesis_param(self):
|
||||
# Minimal stand-in — pycardano's builder uses this for slot math.
|
||||
from pycardano import GenesisParameters
|
||||
|
||||
return GenesisParameters(
|
||||
active_slots_coefficient=0.05,
|
||||
update_quorum=5,
|
||||
max_lovelace_supply=45_000_000_000_000_000,
|
||||
network_magic=764_824_073,
|
||||
epoch_length=432_000,
|
||||
system_start=1_506_203_091,
|
||||
slots_per_kes_period=129_600,
|
||||
slot_length=1,
|
||||
max_kes_evolutions=62,
|
||||
security_param=2_160,
|
||||
)
|
||||
|
||||
@property
|
||||
def era(self):
|
||||
from pycardano import Era
|
||||
return Era.CONWAY
|
||||
|
||||
def utxos(self, address):
|
||||
# Provide one fat UTxO so the builder has inputs to draw fees + min-ADA from.
|
||||
addr = (
|
||||
address
|
||||
if isinstance(address, Address)
|
||||
else Address.from_primitive(address)
|
||||
)
|
||||
tx_in = TransactionInput(
|
||||
transaction_id=TransactionId.from_primitive(bytes.fromhex("cc" * 32)),
|
||||
index=0,
|
||||
)
|
||||
# 1000 ADA, no native assets — plenty for fees + NFT min-UTxO.
|
||||
output = TransactionOutput(addr, Value(1_000_000_000))
|
||||
return [UTxO(tx_in, output)]
|
||||
|
||||
def submit_tx(self, tx): # pragma: no cover — not invoked in these tests
|
||||
pass
|
||||
|
||||
return StubContext()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test vectors for the envelope
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_envelope_from_example-studio_order_vector() -> None:
|
||||
"""A realistic example-studio cert — image CID + studio properties."""
|
||||
md = build_cip25_metadata(
|
||||
policy_id="4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d",
|
||||
asset_name="ExampleStudioCert0042",
|
||||
name="Example Studio Cert #0042",
|
||||
image_cid="bafybeihkop... real cid would be 59 base32 chars",
|
||||
description=(
|
||||
"Certificate of authenticity for hand-stitched custom moth pendant "
|
||||
"ordered by a customer, completed by Sulkta Studio."
|
||||
),
|
||||
media_type="image/png",
|
||||
properties={
|
||||
"studio": "example-studio",
|
||||
"artisan": "Sulkta Studio",
|
||||
"order_id": "CC-2026-0042",
|
||||
"edition": "1 of 1",
|
||||
"material": "sterling silver + polymer clay",
|
||||
},
|
||||
)
|
||||
|
||||
label = md["721"]
|
||||
nft = label[
|
||||
"4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d"
|
||||
]["ExampleStudioCert0042"]
|
||||
|
||||
assert label["version"] == "2.0"
|
||||
assert nft["name"] == "Example Studio Cert #0042"
|
||||
assert nft["mediaType"] == "image/png"
|
||||
assert nft["studio"] == "example-studio"
|
||||
assert nft["artisan"] == "Sulkta"
|
||||
assert nft["edition"] == "1 of 1"
|
||||
|
||||
|
||||
def test_envelope_roundtrips_through_pycardano_metadata() -> None:
|
||||
"""CBOR-encode and decode — the wallet-visible path."""
|
||||
from pycardano import AuxiliaryData, Metadata
|
||||
|
||||
raw = build_cip25_metadata(
|
||||
policy_id="ab" * 28,
|
||||
asset_name="TestNFT",
|
||||
name="Test NFT",
|
||||
image_cid="bafybeitestcidforround-tripping",
|
||||
description="round trip",
|
||||
)
|
||||
# pycardano's Metadata expects int keys at the top level.
|
||||
inner = {int(k): v for k, v in raw.items()}
|
||||
md = Metadata(inner)
|
||||
aux = AuxiliaryData(md)
|
||||
|
||||
cbor_hex = aux.to_cbor_hex()
|
||||
# Must be valid hex and non-trivially sized.
|
||||
assert len(cbor_hex) > 40
|
||||
assert all(c in "0123456789abcdef" for c in cbor_hex)
|
||||
|
||||
# Decoding back must yield the same metadata.
|
||||
decoded = AuxiliaryData.from_cbor(bytes.fromhex(cbor_hex))
|
||||
decoded_md = decoded.data if hasattr(decoded, "data") else decoded # pycardano compat
|
||||
assert decoded_md is not None
|
||||
|
||||
|
||||
def test_envelope_version_key_always_present() -> None:
|
||||
md = build_cip25_metadata(
|
||||
policy_id="a" * 56, asset_name="x", name="n", image_cid="c"
|
||||
)
|
||||
assert md["721"]["version"] == "2.0"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# mint_nft_cert — full tx body construction against a stubbed context
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_mint_nft_cert_returns_unsigned_bundle() -> None:
|
||||
from pycardano import Address, Network, PaymentKeyPair, StakeKeyPair
|
||||
|
||||
script, signer_hashes = _test_policy()
|
||||
policy_cbor_hex = script.to_cbor_hex()
|
||||
# policy_id is blake2b-224 of the script CBOR — use pycardano to compute.
|
||||
policy_id = script.hash().payload.hex()
|
||||
|
||||
# Build two throwaway addresses in testnet namespace.
|
||||
pay_key = PaymentKeyPair.generate()
|
||||
stk_key = StakeKeyPair.generate()
|
||||
funding_addr = str(
|
||||
Address(
|
||||
payment_part=pay_key.verification_key.hash(),
|
||||
staking_part=stk_key.verification_key.hash(),
|
||||
network=Network.TESTNET,
|
||||
)
|
||||
)
|
||||
recipient_pay = PaymentKeyPair.generate()
|
||||
recipient_stk = StakeKeyPair.generate()
|
||||
recipient_addr = str(
|
||||
Address(
|
||||
payment_part=recipient_pay.verification_key.hash(),
|
||||
staking_part=recipient_stk.verification_key.hash(),
|
||||
network=Network.TESTNET,
|
||||
)
|
||||
)
|
||||
|
||||
from cardano_checkout import MintPolicy
|
||||
|
||||
policy = MintPolicy(
|
||||
policy_id=policy_id,
|
||||
script_cbor_hex=policy_cbor_hex,
|
||||
required_signer_hashes=signer_hashes,
|
||||
)
|
||||
|
||||
metadata = build_cip25_metadata(
|
||||
policy_id=policy_id,
|
||||
asset_name="TestCert01",
|
||||
name="Test Cert 01",
|
||||
image_cid="bafybeitest",
|
||||
)
|
||||
|
||||
result = await mint_nft_cert(
|
||||
policy=policy,
|
||||
asset_name="TestCert01",
|
||||
metadata=metadata,
|
||||
recipient_address=recipient_addr,
|
||||
funding_address=funding_addr,
|
||||
context=_stub_context(),
|
||||
network="testnet",
|
||||
)
|
||||
|
||||
assert isinstance(result, UnsignedMint)
|
||||
assert len(result.tx_id) == 64 # hex-encoded 32-byte blake2b hash
|
||||
assert result.tx_body_cbor_hex
|
||||
assert all(c in "0123456789abcdef" for c in result.tx_body_cbor_hex)
|
||||
assert result.auxiliary_data_cbor_hex
|
||||
assert result.native_script_cbor_hex == policy_cbor_hex
|
||||
assert result.required_signer_hashes == signer_hashes
|
||||
assert policy_id in result.summary
|
||||
assert recipient_addr in result.summary
|
||||
|
||||
|
||||
async def test_mint_nft_cert_rejects_oversize_asset_name() -> None:
|
||||
from cardano_checkout import MintPolicy
|
||||
|
||||
policy = MintPolicy(
|
||||
policy_id="aa" * 28,
|
||||
script_cbor_hex="82008200581c" + "11" * 28, # doesn't matter — builder never reached
|
||||
required_signer_hashes=[],
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="asset_name"):
|
||||
await mint_nft_cert(
|
||||
policy=policy,
|
||||
asset_name="X" * 33, # 33 > 32 byte limit
|
||||
metadata={"721": {}},
|
||||
recipient_address="addr_test1...",
|
||||
funding_address="addr_test1...",
|
||||
context=_stub_context(),
|
||||
network="testnet",
|
||||
)
|
||||
|
|
@ -53,23 +53,31 @@ def _utxo(lovelace: int, tx_hash: str = "aa" * 32) -> dict:
|
|||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch_koios_and_oracle(monkeypatch):
|
||||
"""Default: no UTxOs, oracle returns $0.45/ADA. Individual tests override."""
|
||||
def _patch_koios(monkeypatch):
|
||||
"""Default: Koios returns no UTxOs. Individual tests override."""
|
||||
|
||||
async def fake_utxos(address, koios_url=None, timeout=None):
|
||||
return []
|
||||
|
||||
async def fake_price():
|
||||
return 0.45
|
||||
monkeypatch.setattr(monitor, "check_address_utxos", fake_utxos)
|
||||
|
||||
async def fake_convert(usd):
|
||||
|
||||
@pytest.fixture
|
||||
def price_fn_at_45c():
|
||||
"""A deterministic price_fn for tests — USD priced at $0.45/ADA."""
|
||||
async def _convert(usd: float) -> int:
|
||||
if usd <= 0:
|
||||
return 0
|
||||
return int((usd / 0.45) * 1_000_000)
|
||||
return _convert
|
||||
|
||||
monkeypatch.setattr(monitor, "check_address_utxos", fake_utxos)
|
||||
monkeypatch.setattr(monitor, "get_ada_usd_price", fake_price)
|
||||
monkeypatch.setattr(monitor, "convert_usd_to_lovelace", fake_convert)
|
||||
|
||||
@pytest.fixture
|
||||
def price_fn_zero():
|
||||
"""A price_fn that returns 0 — stand-in for oracle unavailability."""
|
||||
async def _zero(usd: float) -> int:
|
||||
return 0
|
||||
return _zero
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -184,14 +192,14 @@ async def test_already_expired_invoices_are_skipped(monkeypatch) -> None:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_reprice_updates_expected_lovelace_and_extends_expiry() -> None:
|
||||
async def test_reprice_updates_expected_lovelace_and_extends_expiry(price_fn_at_45c) -> None:
|
||||
store = InMemoryStore()
|
||||
inv = _make(expected_lovelace=5_000_000)
|
||||
inv.expires_at = datetime.now(timezone.utc) - timedelta(minutes=1)
|
||||
await store.create(inv)
|
||||
|
||||
updated = await monitor.reprice_expired_invoices(
|
||||
store, window_minutes=15, max_repricings=3
|
||||
store, price_fn=price_fn_at_45c, window_minutes=15, max_repricings=3
|
||||
)
|
||||
assert updated == 1
|
||||
|
||||
|
|
@ -205,14 +213,14 @@ async def test_reprice_updates_expected_lovelace_and_extends_expiry() -> None:
|
|||
assert fetched.metadata["repriced_count"] == 1
|
||||
|
||||
|
||||
async def test_reprice_gives_up_after_max_repricings() -> None:
|
||||
async def test_reprice_gives_up_after_max_repricings(price_fn_at_45c) -> None:
|
||||
store = InMemoryStore()
|
||||
inv = _make(expected_lovelace=5_000_000, repriced_count=3)
|
||||
inv.expires_at = datetime.now(timezone.utc) - timedelta(minutes=1)
|
||||
await store.create(inv)
|
||||
|
||||
await monitor.reprice_expired_invoices(
|
||||
store, window_minutes=15, max_repricings=3
|
||||
store, price_fn=price_fn_at_45c, window_minutes=15, max_repricings=3
|
||||
)
|
||||
|
||||
fetched = await store.get("inv")
|
||||
|
|
@ -220,26 +228,21 @@ async def test_reprice_gives_up_after_max_repricings() -> None:
|
|||
assert fetched.status == InvoiceStatus.EXPIRED
|
||||
|
||||
|
||||
async def test_reprice_noop_when_nothing_expired() -> None:
|
||||
async def test_reprice_noop_when_nothing_expired(price_fn_at_45c) -> None:
|
||||
store = InMemoryStore()
|
||||
await store.create(_make(expired_in_minutes=15) if False else _make())
|
||||
await store.create(_make())
|
||||
|
||||
updated = await monitor.reprice_expired_invoices(store)
|
||||
updated = await monitor.reprice_expired_invoices(store, price_fn=price_fn_at_45c)
|
||||
assert updated == 0
|
||||
|
||||
|
||||
async def test_reprice_skips_when_oracle_unavailable(monkeypatch) -> None:
|
||||
async def test_reprice_skips_when_oracle_returns_zero(price_fn_zero) -> None:
|
||||
store = InMemoryStore()
|
||||
inv = _make(expected_lovelace=5_000_000)
|
||||
inv.expires_at = datetime.now(timezone.utc) - timedelta(minutes=1)
|
||||
await store.create(inv)
|
||||
|
||||
async def zero_price():
|
||||
return 0.0
|
||||
|
||||
monkeypatch.setattr(monitor, "get_ada_usd_price", zero_price)
|
||||
|
||||
updated = await monitor.reprice_expired_invoices(store)
|
||||
updated = await monitor.reprice_expired_invoices(store, price_fn=price_fn_zero)
|
||||
assert updated == 0
|
||||
|
||||
fetched = await store.get("inv")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue