Skip to content

The conformance suite

The asset-extension contract in executable form. Prose describing what a backend must do is a wish; these classes are the same statements as tests, and they ship inside the installed package so a fork can run its own backend against the identical suite CI runs.

Adding your own asset is the guide. This page is about the suite itself: what it asserts, how to run it, and why you may not edit it.

Running it

Subclass the contract class and supply the fixtures it asks for. Nothing else: every test is inherited.

from crypto_processing_api.testing.contracts import AutomatedBackendContract


class TestMyBackend(AutomatedBackendContract):
    @pytest.fixture
    def backend(self, my_gateway):
        return MyPayoutBackend(my_gateway)

    @pytest.fixture
    def withdrawal(self, session):
        return make_a_withdrawal(session)

    @pytest.fixture
    def simulate_completion(self, my_gateway):
        return lambda ref: my_gateway.finish(ref)

tests/integration/test_backend_contracts.py is the worked example. Both shipped backends subclass the same classes you will, so reading it tells you what a real set of fixtures looks like.

The suite imports nothing from tests/, on purpose. A contract that only runs inside this repository's test tree is not a contract anybody else can use.

The five contracts

Class For Fixtures you supply
AutomatedBackendContract a backend that sends money by itself backend, withdrawal, simulate_completion
OperatorBackendContract a backend where a human sends the money and code verifies it backend, paying_transaction, near_misses
FeePolicyContract the arithmetic the ledger depends on policy, workable_gross, dust_gross
CustodySourceContract "how much do we actually hold" source, broken_source
EndToEndLedgerContract the books, with your backend plugged in run_withdrawal

What each one is asserting, in one line apiece:

AutomatedBackendContract — a reference that survives a crash and can be found again; unclaimed payouts to the same destination are visible, so nothing pays twice; polling changes nothing; no gateway vocabulary escapes into the state machine; a completed payout reports a transaction id; amounts are never floats; the payout carries our correlation metadata, so recovery resolves by echoed id rather than by guessing from destination and amount; and cancel answers a boolean and never raises.

OperatorBackendContract — references are unique, because two withdrawals sharing one would settle each other; the exact paying transaction verifies; every named near-miss is refused (wrong recipient, wrong amount, wrong sender, wrong token); an unknown transaction is refused; and confirmations never go negative, because a reorg can put the chain tip behind a block you already saw.

FeePolicyContract — net plus fee equals gross; committed equals net plus the wallet fee; nothing is ever negative; the fee never exceeds the amount; every number is an integer; and dust is refused at quote time, so a doomed withdrawal never takes a hold.

CustodySourceContract — a healthy source reports integer units and names itself, and an unavailable one answers None, never 0. That last assertion is the most important line in the file. Zero means "we hold nothing", which is an insolvency emergency; an unreachable API means "we do not know". A source that returns zero when its upstream is down pages somebody at 3am about a wallet that is perfectly fine, and — far worse — trains them to ignore the alert.

EndToEndLedgerContract — the one that matters. Drive a real withdrawal from hold to settle through a real ledger with your backend plugged in, then assert the books still balance and every custody line still reconciles. The four contracts above check a backend in isolation; this checks the claim anybody actually cares about.

Passing it unmodified is the acceptance test

If you find yourself wanting to change contracts.py to make your backend pass, that is the contract telling you something. Every assertion in it is about a property the money path already relies on — none of them are style. If one fails, something in services/ will misbehave in a way that costs coins, and the docstring on each test says which.

The same rule holds for this repository. The two shipped backends (BtcpayPayoutBackend, automated; ManualTronBackend, operator-verified) both run the suite in CI, so a change to the contract that breaks either is caught before it reaches a third one.