From fc337dae521bf976bb9443a143b7dca52fc45d13 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 12 Oct 2026 22:37:20 +0200 Subject: [PATCH] fix: enable rc74 proposal replacement in qa fixtures --- .gitignore | 1 + CHANGELOG.md | 1 + README.md | 89 +++++- docker-compose.yml | 25 +- marketplace/pubky-testnet/Dockerfile | 2 +- payment-requests/Cargo.lock | 20 +- payment-requests/Cargo.toml | 6 +- payment-requests/prepare | 12 +- payment-requests/proposal-smoke.py | 88 ++++++ payment-requests/src/main.rs | 393 +++++++++++++++++++++------ pubky-marketplace | 8 +- 11 files changed, 530 insertions(+), 115 deletions(-) create mode 100644 payment-requests/proposal-smoke.py diff --git a/.gitignore b/.gitignore index 3e31080..c80bb2d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ __pycache__/ .shop-order/ .usdt/ +.qa-pubky-mnemonic diff --git a/CHANGELOG.md b/CHANGELOG.md index 45cbaef..2bcc24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Simplify LND funding step in README to a single command instead of clipboard-based two-step flow ### Fixed +- Enable rc74 payment request fixture linking and same-ID proposal replacement - Pair the Shop and Lock Server denomination-aware setup API, including the seller provisioning helper - Bound the Shop build Node heap and serialize app builds to limit peak Docker memory use - Preserve the payment server's network namespace when starting Shop apps so the USDT verifier keeps its RPC connection diff --git a/README.md b/README.md index 3bee08c..082ab21 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ payment controls, proof verification, the optional Shop overlay and simulation l - **VSS Server**: Versioned Storage Server for app and ldk-node state backups - **Homegate**: Pubky Homeserver signup gatekeeper with local admin API mock - **Pubky marketplace fixture** (opt-in `marketplace` profile): Pubky testnet, Paykit Server and a purchase driver for the marketplace wallet journey -- **Payment Request fixture** (opt-in `payment-requests` profile): rc65 issuer and controlled peer on the marketplace Pubky testnet +- **Payment Request fixture** (opt-in `payment-requests` profile): rc74 issuer and controlled peer on the marketplace Pubky testnet ## Quick Start @@ -311,7 +311,7 @@ docker compose logs -f bitcoind #### Payment Requests and Deadline History -The `payment-requests` profile starts two disposable Paykit rc65 SDK peers (paykit-rs `7185ae7`, Pubky 0.14.0) on +The `payment-requests` profile starts two disposable Paykit rc74 SDK peers (paykit-rs `03d4f05721e92d3944cb4d5af4b2c98c4390a5a8`, Pubky 0.15.0) on the marketplace fixture's Pubky testnet. `fixture-issuer` publishes a regtest Paykit endpoint and sends one-time requests. `rc56-peer` can accept, reject, cancel and pay requests through the shared regtest Bitcoin node. Plain @@ -321,7 +321,7 @@ cancel and pay requests through the shared regtest Bitcoin node. Plain ```bash ./pubky-marketplace up ./pubky-marketplace seed -docker compose --profile marketplace --profile payment-requests build fixture-issuer +docker compose --profile marketplace --profile payment-requests build pubky-testnet homeserver-proxy fixture-issuer docker compose --profile marketplace --profile payment-requests up -d --no-build fixture-issuer rc56-peer for port in 23012 23013; do until health=$(curl -fsS "http://127.0.0.1:$port/health"); do sleep 2; done @@ -398,13 +398,92 @@ Pubky testnet's network namespace, so `./pubky-marketplace down` and `reset` remove them together with the testnet. After `reset`, start them again with the `up -d --no-build fixture-issuer rc56-peer` command above, wait for `/health`, rerun `payment-requests/prepare` and relink the app. `./pubky-marketplace seed` -needs outbound internet for Paykit Server setup; the rc65 peer calls use the +needs outbound internet for Paykit Server setup; the rc74 peer calls use the local testnet. The lane still needs a Bitkit build pointed at the local Pubky testnet and to verify the requested rows on device. The headless preparation command does not populate a separate Bitkit identity's history; accepted and paid app rows require the lane's controlled client to prepare those records with the app's identity or an app build that supports importing fixture state. +##### Proposal identity controls + +The rc74 fixture requires the local Pubky 0.15 homeserver. Build and start +`pubky-testnet`, `homeserver-proxy`, `fixture-issuer` and `rc56-peer` from this +checkout in the test's own Compose project. Preserve the seat's generated +Compose overrides and port mappings; never restart a different project's services. +`/health` reports `paykit_rs_rev` and the available controls, and both peer +services have a Compose healthcheck. Before opening Contact Pay, finish +`payment-requests/prepare --link-only`; then establish the app's own link with +the issuer and verify `/sync` reports `link: linked`. A linking SDK report is +progress, not a completed link. All fixture calls below need a client timeout. +The fixture cannot link an app pointed at staging to its local testnet. + +Check the fixture's controls with two fresh disposable peers (no app or device): + +```bash +python3 payment-requests/proposal-smoke.py \ + --issuer "http://127.0.0.1:${FIXTURE_ISSUER_PORT:-23012}" \ + --peer "http://127.0.0.1:${RC56_PEER_PORT:-23013}" +# On another fresh pair, add --monthly to check a recurring proposal. +``` + +The smoke checks that a request keeps its exact terms and request ID, receives +a new proposal stream ID after forgetting and relinking, refuses the old ID, +and accepts the current ID. It consumes the pair's disposable history; do not +rerun it on a pair that has already accepted a request. + +`POST /replace` takes `peer_pubky`, `peer_path` and `payment_request_id`. It +resends the original issued terms under that request ID with a fresh Event ID. +It returns both Event IDs and refuses an unknown request ID. This endpoint +alone creates a duplicate-proposal conflict if the receiver still retains the +original. For a true replacement, use the SDK's blocked-peer forget flow on +the receiving disposable identity first, then establish a fresh link: + +1. Issue a request, receive it, and sync the issuer to persist its delivery + confirmation. Save the receiver's `proposal_stream_item_id` from `/records`. +2. Keep the app's review open. On the receiver, block, forget, and unblock the + issuer through `/forget` (or the imported-identity command below). On the + issuer, call `/reset-link` for that receiver. It drops only the transport link. +3. Drive `/link` and `/sync` on both sides until both report `linked`. +4. Call issuer `/replace`, receive it on the receiver, and inspect `/records`: + the same request ID must be `Proposed`, with unchanged terms and a different + non-null proposal stream ID. A conflict is not a successful replacement. + +The `/accept` control accepts `expected_proposal_stream_item_id` so a fixture +client can retain an earlier reviewed ID. Omitting it uses the current ID for +ordinary history preparation. The `/forget` SDK operation refuses identities +that already acted on the peer's requests or have active execution claims; +use an unaccepted disposable request. + +For a lane's disposable app identity, the same binary can perform headless +link and history control. Supply that identity's **Pubky** mnemonic, not an +unverified Bitcoin wallet mnemonic, in a temporary test-data file under the +seat checkout. Use the app's published Paykit App ID as `APP_ID` and its known +public key as `EXPECTED_PUBKY`. The controller refuses a mismatched identity +before initializing SDK state, and does not republish its app or endpoints. +Do not use a personal wallet or a host credential store. + +```bash +# APP_PUBKY, APP_ID, ISSUER_PUBKY and the disposable mnemonic file come from this test run. +# COMPOSE_FILE and COMPOSE_PROJECT_NAME remain the seat's own values. +CONTROL_INPUT=$(jq -nc --arg pubky "$ISSUER_PUBKY" '{peer_pubky:$pubky,peer_path:"paykit-server"}') +docker compose --profile marketplace --profile payment-requests run --rm -T --no-deps \ + -e MNEMONIC_FILE=/test-pubky-mnemonic -e EXPECTED_PUBKY="$APP_PUBKY" -e APP_ID="$APP_ID" \ + -v "$PWD/.qa-pubky-mnemonic:/test-pubky-mnemonic:ro" \ + fixture-issuer --control link <<<"$CONTROL_INPUT" +# Repeat with --control sync, records, or forget. Each invocation returns the verified app pubky and result. +``` + +The imported controller uses the same shared SDK storage as the disposable +app, so it can advance the link while a sheet remains open. Device installation +and the app's consumption of that state must be verified on the test run's lane. + +A missing `proposal_stream_item_id` is not a field an issuer can omit in a +wire proposal: the receiving SDK assigns it from its local stream. This fixture +delivers real proposals and cannot synthesize a malformed app-service DTO with +terms but no received proposal ID. That case needs an app-side record injection +seam; a stalled link or a duplicate-proposal conflict does not prove it. + ##### Withholding the issuer's endpoints A journey that needs the app's request resolution to fail (for example @@ -433,7 +512,7 @@ attempt resolves it. Without `peer_pubky`, only the public endpoint changes. #### Following the apps' Paykit pin The Paykit fixtures must run the paykit-rs version the app under test pins: the payment request peers -(`payment-request-fixture:rc65-shared`, paykit-rs `7185ae7`, v0.1.0-rc65) and Paykit Server (built from +(`payment-request-fixture:rc74-shared`, paykit-rs `03d4f05`, v0.1.0-rc74) and Paykit Server (built from the head of [pubky/paykit-server#46](https://github.com/pubky/paykit-server/pull/46), `0ffd4da`, until it merges or is released). Each image records the paykit-rs commit it was built from (label `tech.masivo.paykit-rs`, or `/usr/local/share/paykit-rs-rev` in the peers' image). diff --git a/docker-compose.yml b/docker-compose.yml index ac222cf..2304d9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -472,9 +472,8 @@ services: pubky-testnet: profiles: [marketplace] container_name: marketplace-pubky-testnet - # The homeserver of 0.14.0 answers the Pubky SDK's LOCK and UNLOCK write locks that current Bitkit builds take before writing Paykit state; the - # earlier Pubky Core pin (f68014c1) answered 405. - image: bitkit-docker/pubky-testnet:0.14.0 + # Shared SDK state in rc74 requires homeserver 0.15 or newer, including write-lock enforcement. + image: bitkit-docker/pubky-testnet:0.15.0 build: context: ./marketplace/pubky-testnet restart: "no" # the fixture is disposable: a restarted testnet loses its accounts @@ -559,7 +558,7 @@ services: context: ./marketplace/driver additional_contexts: paykit: service:paykit-server - fixture: docker-image://bitkit-docker/payment-request-fixture:rc65-shared + fixture: docker-image://bitkit-docker/payment-request-fixture:rc74-shared labels: tech.masivo.paykit-server: ${PAYKIT_SERVER_REV:-0ffd4da2adaab048939e0ea18ff916a27a7cfb0e} tech.masivo.paykit-rs: ${PAYKIT_RS_REV:-7185ae7da9315028e5331442d71d739c27f1442c} @@ -581,7 +580,7 @@ services: # (6298) delays or fails the requests of one identity and path (README, Homeserver proxy). Without rules it only forwards. homeserver-proxy: profiles: [marketplace] - image: bitkit-docker/payment-request-fixture:rc65-shared + image: bitkit-docker/payment-request-fixture:rc74-shared build: ./payment-requests restart: on-failure network_mode: service:pubky-testnet @@ -589,16 +588,21 @@ services: - pubky-testnet entrypoint: ["/usr/local/bin/homeserver-proxy"] - # The rc65 SDK peers (paykit-rs 7185ae7, the version both apps pin) share the marketplace testnet namespace and regtest chain. The service name `rc56-peer` stays: lanes and journeys address the peer by it. + # The rc74 SDK peers (paykit-rs 03d4f05) share the marketplace testnet namespace and regtest chain. The service name `rc56-peer` stays: lanes and journeys address the peer by it. fixture-issuer: profiles: [payment-requests] - image: bitkit-docker/payment-request-fixture:rc65-shared + image: bitkit-docker/payment-request-fixture:rc74-shared build: ./payment-requests restart: "no" network_mode: service:pubky-testnet depends_on: - pubky-testnet - bitcoind + healthcheck: + test: ["CMD", "/usr/local/bin/fixture", "--health"] + interval: 2s + timeout: 6s + retries: 75 environment: FIXTURE_ROLE: fixture-issuer FIXTURE_PORT: 3012 @@ -606,13 +610,18 @@ services: rc56-peer: profiles: [payment-requests] - image: bitkit-docker/payment-request-fixture:rc65-shared + image: bitkit-docker/payment-request-fixture:rc74-shared build: ./payment-requests restart: "no" network_mode: service:pubky-testnet depends_on: - pubky-testnet - bitcoind + healthcheck: + test: ["CMD", "/usr/local/bin/fixture", "--health"] + interval: 2s + timeout: 6s + retries: 75 environment: FIXTURE_ROLE: rc56-peer FIXTURE_PORT: 3013 diff --git a/marketplace/pubky-testnet/Dockerfile b/marketplace/pubky-testnet/Dockerfile index 307cd8c..6b44374 100644 --- a/marketplace/pubky-testnet/Dockerfile +++ b/marketplace/pubky-testnet/Dockerfile @@ -5,7 +5,7 @@ # one before it writes shared state); the earlier Pubky Core pin answered 405. FROM rust:1.98.1-bookworm AS builder -ARG PUBKY_TESTNET_VERSION=0.14.0 +ARG PUBKY_TESTNET_VERSION=0.15.0 RUN cargo install pubky-testnet --version "${PUBKY_TESTNET_VERSION}" --locked --root /opt/pubky FROM debian:bookworm-slim diff --git a/payment-requests/Cargo.lock b/payment-requests/Cargo.lock index c434695..a23d024 100644 --- a/payment-requests/Cargo.lock +++ b/payment-requests/Cargo.lock @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "paykit-lib" -version = "0.1.0-rc65" -source = "git+https://github.com/pubky/paykit-rs.git?rev=7185ae7da9315028e5331442d71d739c27f1442c#7185ae7da9315028e5331442d71d739c27f1442c" +version = "0.1.0-rc74" +source = "git+https://github.com/pubky/paykit-rs.git?rev=03d4f05721e92d3944cb4d5af4b2c98c4390a5a8#03d4f05721e92d3944cb4d5af4b2c98c4390a5a8" dependencies = [ "anyhow", "base64 0.22.1", @@ -1711,8 +1711,8 @@ dependencies = [ [[package]] name = "paykit-sdk" -version = "0.1.0-rc65" -source = "git+https://github.com/pubky/paykit-rs.git?rev=7185ae7da9315028e5331442d71d739c27f1442c#7185ae7da9315028e5331442d71d739c27f1442c" +version = "0.1.0-rc74" +source = "git+https://github.com/pubky/paykit-rs.git?rev=03d4f05721e92d3944cb4d5af4b2c98c4390a5a8#03d4f05721e92d3944cb4d5af4b2c98c4390a5a8" dependencies = [ "anyhow", "async-trait", @@ -1934,9 +1934,9 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "pubky" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2099d521cacff9a1bf2ad48978d4601608d80860d7b08f7ecb0ced98bb63b628" +checksum = "c298b2ec3f482618764c4bdc5cd69a9e52515e95e315d8f2dd946b83e4d03362" dependencies = [ "async-trait", "base64 0.23.1", @@ -1966,9 +1966,9 @@ dependencies = [ [[package]] name = "pubky-common" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9b32c46e101fa9b94fcb3f3baa5751d3f696b3823508697a4f407281cbe9c4c" +checksum = "e4a8b4c4831885f025577109215022f7e785b8c421b516cdfe1b510c8f2653fe" dependencies = [ "argon2", "base64 0.23.1", @@ -1988,9 +1988,9 @@ dependencies = [ [[package]] name = "pubky-noise" -version = "0.1.0-rc12" +version = "0.1.0-rc13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c034fc7c278e85960a566a032ee26acf9d9c00ee6f64d74fa1f73ee9ed7d4f06" +checksum = "d05863db1f5683e6c539c384eb7d9372451faad4770916b279bf6e1de2a5008a" dependencies = [ "chacha20poly1305", "curve25519-dalek 5.0.0", diff --git a/payment-requests/Cargo.toml b/payment-requests/Cargo.toml index ba5d31e..7a81987 100644 --- a/payment-requests/Cargo.toml +++ b/payment-requests/Cargo.toml @@ -9,9 +9,9 @@ async-trait = "0.1" axum = "0.8" base64 = "0.22" chrono = "0.4" -paykit-lib = { git = "https://github.com/pubky/paykit-rs.git", rev = "7185ae7da9315028e5331442d71d739c27f1442c" } -paykit-sdk = { git = "https://github.com/pubky/paykit-rs.git", rev = "7185ae7da9315028e5331442d71d739c27f1442c" } -pubky = "=0.14.0" +paykit-lib = { git = "https://github.com/pubky/paykit-rs.git", rev = "03d4f05721e92d3944cb4d5af4b2c98c4390a5a8" } +paykit-sdk = { git = "https://github.com/pubky/paykit-rs.git", rev = "03d4f05721e92d3944cb4d5af4b2c98c4390a5a8" } +pubky = "=0.15.0" pkarr = { version = "8.1", features = ["tls"] } reqwest = { version = "0.13", default-features = false, features = ["json", "rustls", "stream"] } serde = { version = "1", features = ["derive"] } diff --git a/payment-requests/prepare b/payment-requests/prepare index e1fd0ca..a92e0d2 100755 --- a/payment-requests/prepare +++ b/payment-requests/prepare @@ -20,7 +20,7 @@ fail() { # --fail-with-body keeps the peer's JSON error; show it with the failing call instead of a bare HTTP code. post() { local response - if ! response=$(curl --fail-with-body -sS -X POST "http://127.0.0.1:$1$2" -H 'content-type: application/json' -d "$3" 2>&1); then + if ! response=$(curl --max-time 20 --fail-with-body -sS -X POST "http://127.0.0.1:$1$2" -H 'content-type: application/json' -d "$3" 2>&1); then echo "prepare: POST :$1$2 failed: $response" >&2 return 1 fi @@ -30,7 +30,7 @@ post() { # The peers publish their identities before they listen. Until then Docker's port proxy refuses or resets the # connection (curl --retry retries only the refusal), so poll /health for up to 150 seconds. wait_health() { - until curl -fsS -o /dev/null "http://127.0.0.1:$1/health" 2>/dev/null; do + until curl --max-time 5 -fsS -o /dev/null "http://127.0.0.1:$1/health" 2>/dev/null; do if [ "$SECONDS" -ge "$deadline" ]; then fail "port $1 did not answer /health; see: docker compose --profile marketplace --profile payment-requests logs fixture-issuer rc56-peer" fi @@ -41,8 +41,8 @@ deadline=$((SECONDS + 150)) wait_health "$issuer_port" wait_health "$peer_port" -issuer=$(curl --fail-with-body -sS http://127.0.0.1:$issuer_port/info) -peer=$(curl --fail-with-body -sS http://127.0.0.1:$peer_port/info) +issuer=$(curl --max-time 20 --fail-with-body -sS http://127.0.0.1:$issuer_port/info) +peer=$(curl --max-time 20 --fail-with-body -sS http://127.0.0.1:$peer_port/info) to_peer=$(jq -nc --arg pubky "$(jq -r .pubky <<<"$peer")" --arg path "$(jq -r .receiver_path <<<"$peer")" '{peer_pubky:$pubky,peer_path:$path}') to_issuer=$(jq -nc --arg pubky "$(jq -r .pubky <<<"$issuer")" --arg path "$(jq -r .receiver_path <<<"$issuer")" '{peer_pubky:$pubky,peer_path:$path}') @@ -63,6 +63,10 @@ for _ in {1..20}; do linked=true break fi + printf 'prepare: issuer link %s; peer link %s\n' \ + "$(jq -r '.link | if type == "object" then .state // "linking" else . end' <<<"$issuer_link")" \ + "$(jq -r '.link | if type == "object" then .state // "linking" else . end' <<<"$peer_link")" >&2 + [ "$SECONDS" -lt "$deadline" ] || break sleep 0.2 done if [ "$linked" != true ]; then diff --git a/payment-requests/proposal-smoke.py b/payment-requests/proposal-smoke.py new file mode 100644 index 0000000..fb5854a --- /dev/null +++ b/payment-requests/proposal-smoke.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Check disposable fixture controls, without a Bitkit app or a device.""" +import argparse +import json +import time +import urllib.error +import urllib.request +import uuid +from datetime import datetime, timezone + +PIN = '03d4f05721e92d3944cb4d5af4b2c98c4390a5a8' + + +def call(base, path, body=None): + data = None if body is None else json.dumps(body).encode() + req = urllib.request.Request(base + path, data=data, headers={'content-type': 'application/json'}) + with urllib.request.urlopen(req, timeout=20) as response: + return json.load(response) + + +def link(issuer, peer, to_peer, to_issuer): + deadline = time.monotonic() + 60 + while time.monotonic() < deadline: + call(issuer, '/link', to_peer) + call(peer, '/link', to_issuer) + a = call(issuer, '/sync', to_peer) + b = call(peer, '/sync', to_issuer) + if a.get('link') == b.get('link') == 'linked': + return + time.sleep(.2) + raise RuntimeError('two disposable peers did not finish a link within 60 seconds') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--issuer', default='http://127.0.0.1:23012') + parser.add_argument('--peer', default='http://127.0.0.1:23013') + parser.add_argument('--monthly', action='store_true') + args = parser.parse_args() + a, b = call(args.issuer, '/health'), call(args.peer, '/health') + assert a['paykit_rs_rev'] == b['paykit_rs_rev'] == PIN + to_peer = {'peer_pubky': b['pubky'], 'peer_path': b['receiver_path']} + to_issuer = {'peer_pubky': a['pubky'], 'peer_path': a['receiver_path']} + link(args.issuer, args.peer, to_peer, to_issuer) + request_id = str(uuid.uuid4()) + request = dict(to_peer, payment_request_id=request_id, amount_sats=15000, + reference='fixture-proposal-identity', no_deadline=True) + if args.monthly: + request['monthly_starts_at'] = datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') + call(args.issuer, '/request', request) + call(args.peer, '/sync', to_issuer) + # Persist delivery confirmation before dropping the link, so the old event is not resent. + call(args.issuer, '/sync', to_peer) + def record(): + rows = call(args.peer, '/records', to_issuer)['records'] + return next(row for row in rows if row['payment_request_id'] == request_id) + old = record() + assert old['state'] == 'Proposed' and old['proposal_stream_item_id'] is not None + call(args.peer, '/forget', to_issuer) + call(args.issuer, '/reset-link', to_peer) + link(args.issuer, args.peer, to_peer, to_issuer) + replaced = call(args.issuer, '/replace', dict(to_peer, payment_request_id=request_id)) + assert replaced['original_event_id'] != replaced['replacement_event_id'] + call(args.peer, '/sync', to_issuer) + new = record() + assert new['state'] == 'Proposed', new + assert old['terms'] == new['terms'] + assert old['proposal_stream_item_id'] != new['proposal_stream_item_id'] + try: + call(args.peer, '/accept', dict(to_issuer, payment_request_id=request_id, + expected_proposal_stream_item_id=old['proposal_stream_item_id'])) + except urllib.error.HTTPError as error: + detail = json.load(error) + assert error.code == 400 and 'expected stream item' in detail['error'], detail + else: + raise AssertionError('fixture accepted an obsolete stream item ID') + accepted = call(args.peer, '/accept', dict(to_issuer, payment_request_id=request_id, + expected_proposal_stream_item_id=new['proposal_stream_item_id'])) + assert accepted['state'] == ('ActiveRecurring' if args.monthly else 'Accepted'), accepted + print(json.dumps({'fixture': 'ready', 'recurring': args.monthly, + 'same_request_id': request_id, 'same_terms': True, + 'old_stream_id': old['proposal_stream_item_id'], + 'new_stream_id': new['proposal_stream_item_id'], + 'obsolete_acceptance': 'refused', 'current_acceptance': accepted['state']})) + + +if __name__ == '__main__': + main() diff --git a/payment-requests/src/main.rs b/payment-requests/src/main.rs index b4e4dbc..356a9d5 100644 --- a/payment-requests/src/main.rs +++ b/payment-requests/src/main.rs @@ -1,4 +1,4 @@ -//! Disposable rc65 Paykit peer for Bitkit regtest journeys. +//! Disposable rc74 Paykit peer for Bitkit regtest journeys. //! Two Compose services run this binary with separate identities and receiver paths. #![recursion_limit = "512"] @@ -19,10 +19,10 @@ use paykit_lib::{ PaymentRequestId, PaymentRequestTerms, Recurrence, RecurrenceConfig, RecurrenceUnit, }; use paykit_sdk::{ - PubkySharedStateStorage, StorageAdapter, LinkedPeerState, PaykitAppCapabilities, PaykitAppId, PaykitApp, PaykitSdk, - PaykitSdkConfig, PaymentAdapter, PaymentRequestLifecycleState, PubkyLocalSecretKey, - PubkyPublicKey, PubkySessionAccess, PubkySessionBootstrap, PubkySessionProvider, - PAYKIT_AUTHORIZER_SESSION_CAPABILITIES, + LinkedPeerState, PaykitApp, PaykitAppCapabilities, PaykitAppId, PaykitSdk, PaykitSdkConfig, + PaymentAdapter, PaymentRequestLifecycleState, PubkyLocalSecretKey, PubkyPublicKey, + PubkySessionAccess, PubkySessionBootstrap, PubkySessionProvider, PubkySharedStateStorage, + StorageAdapter, PAYKIT_AUTHORIZER_SESSION_CAPABILITIES, }; use pubky::{Keypair, Pubky}; use serde::Deserialize; @@ -91,7 +91,10 @@ impl App { payload: json!({ "value": self.address }).to_string(), }]; if let Some(lnurl) = self.lnurl.lock().unwrap().clone() { - details.push(paykit_sdk::PrivateReceivingDetail { identifier: LNURL_ENDPOINT.into(), payload: json!({ "value": lnurl }).to_string() }); + details.push(paykit_sdk::PrivateReceivingDetail { + identifier: LNURL_ENDPOINT.into(), + payload: json!({ "value": lnurl }).to_string(), + }); } details } @@ -100,7 +103,9 @@ impl App { #[derive(Deserialize)] struct Peer { peer_pubky: String, - peer_path: String, + // Kept in the HTTP contract; rc74 links are identity-wide. + #[serde(rename = "peer_path")] + _peer_path: String, } impl Peer { @@ -143,6 +148,8 @@ struct RecordInput { #[serde(flatten)] peer: Peer, payment_request_id: String, + // Optional for normal fixture preparation; supply the reviewed ID to probe stale approval. + expected_proposal_stream_item_id: Option, } #[derive(Deserialize)] @@ -224,7 +231,9 @@ where eprintln!("setup: {what} failed, retrying: {error:#}"); tokio::time::sleep(std::time::Duration::from_secs(2)).await; } - Err(error) => return Err(error.context(format!("{what} still failing after {timeout}s"))), + Err(error) => { + return Err(error.context(format!("{what} still failing after {timeout}s"))) + } } } } @@ -232,8 +241,13 @@ where async fn setup() -> Result { let role = env::var("FIXTURE_ROLE").context("FIXTURE_ROLE is required")?; // the issuer contract of the Bitkit journeys: the fixture issuer's App ID is `paykit-server` - let default_app_id = if role == "fixture-issuer" { "paykit-server" } else { "qa-fixture" }; - let receiver_path = PaykitAppId::new(env::var("APP_ID").unwrap_or_else(|_| default_app_id.into()))?; + let default_app_id = if role == "fixture-issuer" { + "paykit-server" + } else { + "qa-fixture" + }; + let receiver_path = + PaykitAppId::new(env::var("APP_ID").unwrap_or_else(|_| default_app_id.into()))?; let pubky = Pubky::testnet()?; let bootstrap = PubkySessionBootstrap::with_pubky(pubky, "bitkit-docker.fixture")? .with_auth_relay("http://localhost:15412/inbox")?; @@ -242,8 +256,16 @@ async fn setup() -> Result { // A new identity per attempt: a failed sign-up must not leave the retry with a half-created account. let signed_up = retry("sign-up on the local homeserver", || async { let phrase_file = env::var("MNEMONIC_FILE").ok(); - let secret = if let Some(ref file) = phrase_file { PubkyLocalSecretKey::from_bip39_mnemonic(std::fs::read_to_string(file)?.trim())? } else { PubkyLocalSecretKey::new(Keypair::random().secret_key()) }; - if phrase_file.is_some() { return Ok(bootstrap.sign_in(&secret, PAYKIT_AUTHORIZER_SESSION_CAPABILITIES).await?); } + let secret = if let Some(ref file) = phrase_file { + PubkyLocalSecretKey::from_bip39_mnemonic(std::fs::read_to_string(file)?.trim())? + } else { + PubkyLocalSecretKey::new(Keypair::random().secret_key()) + }; + if phrase_file.is_some() { + return Ok(bootstrap + .sign_in(&secret, PAYKIT_AUTHORIZER_SESSION_CAPABILITIES) + .await?); + } Ok(bootstrap .sign_up( &secret, @@ -254,29 +276,32 @@ async fn setup() -> Result { .await?) }) .await?; + if let Ok(expected) = env::var("EXPECTED_PUBKY") { + if signed_up.public_key != PubkyPublicKey::from_raw_or_app_key(&expected)? { + bail!("imported disposable identity does not match EXPECTED_PUBKY"); + } + } let provider = SessionProvider(Arc::new(Mutex::new(Some(signed_up.access)))); let storage = PubkySharedStateStorage::new(provider.clone()); - let sdk = PaykitSdk::new( - storage.clone(), - provider, - FixturePaymentAdapter, - config, - ); + let sdk = PaykitSdk::new(storage.clone(), provider, FixturePaymentAdapter, config); sdk.initialize().await?; let imported = env::var("MNEMONIC_FILE").is_ok(); if !imported { - sdk.publish_paykit_noise_key_authorization().await?; - retry("receiver marker publication", || async { - Ok(sdk - .publish_paykit_app(PaykitApp::new("QA Fixture", PaykitAppCapabilities { - private_payments: true, - payment_requests: true, - receipts: false, - outgoing_payments: role == "rc56-peer", - })?) - .await?) - }) - .await?; + sdk.publish_paykit_noise_key_authorization().await?; + retry("receiver marker publication", || async { + Ok(sdk + .publish_paykit_app(PaykitApp::new( + "QA Fixture", + PaykitAppCapabilities { + private_payments: true, + payment_requests: true, + receipts: false, + outgoing_payments: role == "rc56-peer", + }, + )?) + .await?) + }) + .await?; } let address = retry("getnewaddress from bitcoind", || async { let address = rpc("getnewaddress", json!(["", "bech32"])) @@ -292,19 +317,21 @@ async fn setup() -> Result { .await?; let endpoint_payload = json!({ "value": address }).to_string(); if !imported { - retry("Paykit endpoint publication", || async { - let published = sdk - .sync_public_endpoints_with_receiving_details(vec![paykit_sdk::PublicReceivingDetail { - identifier: ENDPOINT.into(), - payload: endpoint_payload.clone(), - }]) - .await?; - if !published.failed.is_empty() || published.published.len() != 1 { - bail!("Paykit endpoint publication failed"); - } - Ok(()) - }) - .await?; + retry("Paykit endpoint publication", || async { + let published = sdk + .sync_public_endpoints_with_receiving_details(vec![ + paykit_sdk::PublicReceivingDetail { + identifier: ENDPOINT.into(), + payload: endpoint_payload.clone(), + }, + ]) + .await?; + if !published.failed.is_empty() || published.published.len() != 1 { + bail!("Paykit endpoint publication failed"); + } + Ok(()) + }) + .await?; } Ok(App { sdk, @@ -320,10 +347,15 @@ async fn setup() -> Result { } async fn info(State(app): State>) -> ApiResult { + // The Dockerfile can follow an app's SDK pin; report what the image actually built. + let sdk_revision = std::fs::read_to_string("/usr/local/share/paykit-rs-rev") + .unwrap_or_else(|_| "03d4f05721e92d3944cb4d5af4b2c98c4390a5a8".into()); Ok(Json(json!({ "status": "ready", "role": app.role, "pubky": app.pubky.to_app_key(), "receiver_path": app.receiver_path.as_str(), "endpoint": ENDPOINT, "address": app.address, "lnurl": app.lnurl.lock().unwrap().clone(), + "paykit_rs_rev": sdk_revision.trim(), + "controls": ["replace", "forget", "reset-link"], }))) } @@ -351,10 +383,7 @@ async fn sync_locked(app: &App, peer: PubkyPublicKey) -> Result { if state != LinkedPeerState::Linked { bail!("peer link is {state:?}"); } - let received = app - .sdk - .receive_private_messages(peer.clone()) - .await?; + let received = app.sdk.receive_private_messages(peer.clone()).await?; let sent = app .sdk .process_outbound_private_messages(peer.clone()) @@ -391,7 +420,11 @@ async fn issue(State(app): State>, Json(input): Json) -> "btc", )?, PaymentReference::new(input.reference)?, - vec![PaymentEndpointIdentifier::new(if input.lnurl.is_some() { LNURL_ENDPOINT } else { ENDPOINT })?], + vec![PaymentEndpointIdentifier::new(if input.lnurl.is_some() { + LNURL_ENDPOINT + } else { + ENDPOINT + })?], ) .proposal_expires_at(input.proposal_expires_at); if let Some(lnurl) = input.lnurl { @@ -429,7 +462,10 @@ async fn issue(State(app): State>, Json(input): Json) -> // While `/endpoints` withholds them, the list goes out empty: the request names an endpoint the app cannot resolve. let withheld = *app.withheld.lock().await; app.sdk - .enqueue_private_payment_list_with_receiving_details(peer.clone(), app.private_details(withheld)) + .enqueue_private_payment_list_with_receiving_details( + peer.clone(), + app.private_details(withheld), + ) .await?; let mut record = app .sdk @@ -451,16 +487,17 @@ async fn issue(State(app): State>, Json(input): Json) -> context: "fixture proposal not queued".into(), source: None, })?; - message.raw_json = message.raw_json.replace(&generated, &replacement); + message.raw_json = rename_request(&message.raw_json, &generated, &replacement) + .map_err(|error| paykit_sdk::PaykitSdkError::Protocol { + context: error.to_string(), + source: None, + })?; tx.save_outbound_private_message(message) }) .await?; record.payment_request_id = fixed; } - let sent = app - .sdk - .process_outbound_private_messages(peer) - .await?; + let sent = app.sdk.process_outbound_private_messages(peer).await?; if !sent.failed.is_empty() || sent.sent.len() < 2 { return Err(anyhow!( "Payment Request delivery failed: {} failed, {} sent (the private payment list and the request)", @@ -476,6 +513,104 @@ async fn issue(State(app): State>, Json(input): Json) -> )) } +// Change the protocol field only: a reference or metadata may contain the generated UUID too. +fn rename_request(raw: &str, expected: &str, replacement: &str) -> Result { + PaymentRequestId::new(replacement.to_owned())?; + let mut value: Value = serde_json::from_str(raw)?; + if value["payment_request_id"] != expected { + bail!("queued proposal has another Payment Request ID"); + } + value["payment_request_id"] = json!(replacement); + Ok(serde_json::to_string(&value)?) +} + +/// Resend the original terms under the same request ID with a new Event ID. +/// The receiver must forget the old peer history first, or the SDK records a conflict. +async fn replace(State(app): State>, Json(input): Json) -> ApiResult { + let _guard = app.operation.lock().await; + let peer = input.peer.parsed()?; + let id = PaymentRequestId::new(input.payment_request_id)?; + let original = app + .storage + .transaction(|tx| { + Ok(tx + .outbound_private_messages(&peer) + .into_iter() + .find(|message| { + message.kind == "paykit.payment_request" + && serde_json::from_str::(&message.raw_json) + .is_ok_and(|value| value["payment_request_id"] == id.as_str()) + })) + }) + .await? + .context("unknown issued Payment Request")?; + let message = paykit_lib::PrivateApplicationMessage { + version: Some(1), + kind: Some(original.kind), + app_id: Some(original.app_id.as_str().to_owned()), + raw_json: original.raw_json, + }; + let parsed = paykit_lib::parse_payment_request_event_message(&message) + .context("original is not a Payment Request")?; + let Some(paykit_lib::PaymentRequestEvent::Request(request)) = parsed.parsed_event() else { + return Err(anyhow!("original proposal is invalid").into()); + }; + let terms = request.request().clone(); + let original_event = request.event_id().as_str().to_owned(); + let queued = app.sdk.propose_payment_request(peer.clone(), terms).await?; + let generated = queued.payment_request_id; + let replacement = id.as_str().to_owned(); + app.storage + .transaction(|tx| { + let mut message = tx + .queued_outbound_private_messages(&peer) + .into_iter() + .find(|message| { + serde_json::from_str::(&message.raw_json) + .is_ok_and(|value| value["payment_request_id"] == generated) + }) + .ok_or_else(|| paykit_sdk::PaykitSdkError::Protocol { + context: "replacement not queued".into(), + source: None, + })?; + message.raw_json = rename_request(&message.raw_json, &generated, &replacement) + .map_err(|error| paykit_sdk::PaykitSdkError::Protocol { + context: error.to_string(), + source: None, + })?; + tx.save_outbound_private_message(message) + }) + .await?; + let sent = app.sdk.process_outbound_private_messages(peer).await?; + if !sent.failed.is_empty() || sent.sent.is_empty() { + return Err(anyhow!("replacement delivery failed").into()); + } + Ok(Json(json!({ "payment_request_id": id.as_str(), + "original_event_id": original_event, "replacement_event_id": queued.proposal_event_id, + "sent": sent.sent.len() }))) +} + +/// Controlled reset through the SDK's public API, preserving monotonic stream IDs. +/// An imported disposable app identity can use this while its review sheet remains open. +async fn forget(State(app): State>, Json(input): Json) -> ApiResult { + let _guard = app.operation.lock().await; + let peer = input.parsed()?; + app.sdk.block_peer(peer.clone()).await?; + app.sdk.forget_peer(peer.clone()).await?; + let record = app.sdk.unblock_peer(peer).await?; + Ok(Json(serde_json::to_value(record)?)) +} + +/// Drop only the transport link on the issuer before a fresh handshake. +/// Its original proposal and delivery confirmation stay in shared history. +async fn reset_link(State(app): State>, Json(input): Json) -> ApiResult { + let _guard = app.operation.lock().await; + let peer = input.parsed()?; + app.sdk.block_peer(peer.clone()).await?; + let record = app.sdk.unblock_peer(peer).await?; + Ok(Json(serde_json::to_value(record)?)) +} + async fn records(State(app): State>, Json(input): Json) -> ApiResult { let _guard = app.operation.lock().await; let peer = input.parsed()?; @@ -487,16 +622,26 @@ async fn records(State(app): State>, Json(input): Json) -> ApiRes async fn act(State(app): State>, action: &'static str, input: RecordInput) -> ApiResult { let _guard = app.operation.lock().await; let peer = input.peer.parsed()?; - let _ = app - .sdk - .receive_private_messages(peer.clone()) - .await?; + let _ = app.sdk.receive_private_messages(peer.clone()).await?; let id = PaymentRequestId::new(input.payment_request_id)?; let record = match action { "accept" => { - app.sdk.claim_payment_request_for_execution(peer.clone(), &id).await?; + let reviewed = match input.expected_proposal_stream_item_id { + Some(id) => id, + None => app + .sdk + .payment_requests_with(&peer) + .await? + .into_iter() + .find(|record| record.payment_request_id == id.as_str()) + .and_then(|record| record.proposal_stream_item_id) + .context("request has no proposal stream item ID")?, + }; + app.sdk + .claim_payment_request_for_execution(peer.clone(), &id) + .await?; app.sdk - .accept_payment_request(peer.clone(), &id) + .accept_payment_request(peer.clone(), &id, reviewed) .await? } "reject" => { @@ -505,17 +650,16 @@ async fn act(State(app): State>, action: &'static str, input: RecordInp .await? } "cancel" => { - app.sdk.claim_payment_request_for_execution(peer.clone(), &id).await?; + app.sdk + .claim_payment_request_for_execution(peer.clone(), &id) + .await?; app.sdk .cancel_payment_request(peer.clone(), &id, None) .await? } _ => unreachable!(), }; - let sent = app - .sdk - .process_outbound_private_messages(peer) - .await?; + let sent = app.sdk.process_outbound_private_messages(peer).await?; if !sent.failed.is_empty() || sent.sent.len() != 1 { return Err(anyhow!("{action} delivery failed").into()); } @@ -538,10 +682,7 @@ async fn pay_impl(app: Arc, input: PayInput, existing_tx: bool) -> ApiResul let _guard = app.operation.lock().await; let peer = input.record.peer.parsed()?; let id = PaymentRequestId::new(input.record.payment_request_id)?; - let _ = app - .sdk - .receive_private_messages(peer.clone()) - .await?; + let _ = app.sdk.receive_private_messages(peer.clone()).await?; let record = app .sdk .payment_requests_with(&peer) @@ -647,10 +788,19 @@ async fn proof(State(app): State>, Json(input): Json) -> ApiR } async fn withdraw(State(app): State>, Json(input): Json) -> ApiResult { - let _guard = app.operation.lock().await; - let peer = input.parsed()?; - let report = app.sdk.sync_private_payment_lists_with_reservations_and_process_outbound(vec![paykit_sdk::PrivatePaymentListReservationUpdate {counterparty: peer, reservations: vec![]}], false).await?; - Ok(Json(serde_json::to_value(report)?)) + let _guard = app.operation.lock().await; + let peer = input.parsed()?; + let report = app + .sdk + .sync_private_payment_lists_with_reservations_and_process_outbound( + vec![paykit_sdk::PrivatePaymentListReservationUpdate { + counterparty: peer, + reservations: vec![], + }], + false, + ) + .await?; + Ok(Json(serde_json::to_value(report)?)) } #[derive(Deserialize)] @@ -663,7 +813,9 @@ struct EndpointsInput { } async fn endpoints_state(State(app): State>) -> ApiResult { - Ok(Json(json!({ "withheld": *app.withheld.lock().await, "endpoint": ENDPOINT }))) + Ok(Json( + json!({ "withheld": *app.withheld.lock().await, "endpoint": ENDPOINT }), + )) } /// Withholds or restores the issuer's payment endpoints: its public endpoint and, for the named peer, its private payment list. @@ -678,20 +830,35 @@ async fn endpoints(State(app): State>, Json(input): Json = app .private_details(withheld) .into_iter() - .map(|detail| paykit_sdk::PublicReceivingDetail { identifier: detail.identifier, payload: detail.payload }) + .map(|detail| paykit_sdk::PublicReceivingDetail { + identifier: detail.identifier, + payload: detail.payload, + }) .collect(); - let published = app.sdk.sync_public_endpoints_with_receiving_details(public).await?; + let published = app + .sdk + .sync_public_endpoints_with_receiving_details(public) + .await?; let mut private = Value::Null; if let Some(peer) = input.peer { let peer = peer.parsed()?; app.sdk - .enqueue_private_payment_list_with_receiving_details(peer.clone(), app.private_details(withheld)) + .enqueue_private_payment_list_with_receiving_details( + peer.clone(), + app.private_details(withheld), + ) .await?; let sent = app.sdk.process_outbound_private_messages(peer).await?; if !sent.failed.is_empty() || sent.sent.is_empty() { - return Err(anyhow!("private payment list delivery failed: {} failed, {} sent", sent.failed.len(), sent.sent.len()).into()); + return Err(anyhow!( + "private payment list delivery failed: {} failed, {} sent", + sent.failed.len(), + sent.sent.len() + ) + .into()); } - private = json!({ "sent": sent.sent.len(), "entries": app.private_details(withheld).len() }); + private = + json!({ "sent": sent.sent.len(), "entries": app.private_details(withheld).len() }); } Ok(Json(json!({ "withheld": withheld, @@ -723,7 +890,9 @@ async fn publish_key_authorization() -> Result<()> { let secret = PubkyLocalSecretKey::new(bytes); let bootstrap = PubkySessionBootstrap::with_pubky(Pubky::testnet()?, "bitkit-docker.fixture")? .with_auth_relay("http://localhost:15412/inbox")?; - let signed_in = bootstrap.sign_in(&secret, PAYKIT_AUTHORIZER_SESSION_CAPABILITIES).await?; + let signed_in = bootstrap + .sign_in(&secret, PAYKIT_AUTHORIZER_SESSION_CAPABILITIES) + .await?; let pubky = signed_in.public_key.to_app_key(); let provider = SessionProvider(Arc::new(Mutex::new(Some(signed_in.access)))); let sdk = PaykitSdk::new( @@ -744,7 +913,49 @@ async fn main() -> Result<()> { if invoked.ends_with("paykit-key-authorization") { return publish_key_authorization().await; } + if env::args().nth(1).as_deref() == Some("--health") { + let port = env::var("FIXTURE_PORT").unwrap_or_else(|_| "3002".into()); + reqwest::Client::new() + .get(format!("http://127.0.0.1:{port}/health")) + .timeout(std::time::Duration::from_secs(5)) + .send() + .await? + .error_for_status()?; + return Ok(()); + } + let control = env::args().nth(1).as_deref() == Some("--control"); + if control && (env::var("MNEMONIC_FILE").is_err() || env::var("EXPECTED_PUBKY").is_err()) { + bail!("--control requires MNEMONIC_FILE and EXPECTED_PUBKY for a disposable identity"); + } let app = Arc::new(setup().await?); + if control { + let mut body = String::new(); + std::io::Read::read_to_string(&mut std::io::stdin(), &mut body)?; + let peer: Peer = serde_json::from_str(&body)?; + let result = match env::args().nth(2).as_deref() { + Some("forget") => forget(State(app.clone()), Json(peer)).await, + Some("reset-link") => reset_link(State(app.clone()), Json(peer)).await, + Some("link") => { + link( + State(app.clone()), + Json(LinkInput { + peer, + mode: String::new(), + }), + ) + .await + } + Some("sync") => sync(State(app.clone()), Json(peer)).await, + Some("records") => records(State(app.clone()), Json(peer)).await, + _ => bail!("--control action must be forget, reset-link, link, sync or records"), + } + .map_err(|error| error.0)?; + println!( + "{}", + json!({"pubky": app.pubky.to_app_key(), "result": result.0}) + ); + return Ok(()); + } let port: u16 = env::var("FIXTURE_PORT") .unwrap_or_else(|_| "3002".into()) .parse()?; @@ -755,6 +966,9 @@ async fn main() -> Result<()> { .route("/sync", post(sync)) .route("/records", post(records)) .route("/request", post(issue)) + .route("/replace", post(replace)) + .route("/forget", post(forget)) + .route("/reset-link", post(reset_link)) .route("/accept", post(accept)) .route("/reject", post(reject)) .route("/cancel", post(cancel)) @@ -767,3 +981,22 @@ async fn main() -> Result<()> { axum::serve(listener, router).await?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_rename_preserves_reference_and_terms() { + let old = "71300000-0000-4000-8000-000000000001"; + let new = "71300000-0000-4000-8000-000000000002"; + let raw = json!({"payment_request_id": old, "request": {"payment_reference": old}, "event_id": "unchanged"}); + let changed: Value = + serde_json::from_str(&rename_request(&raw.to_string(), old, new).unwrap()).unwrap(); + assert_eq!(changed["payment_request_id"], new); + assert_eq!(changed["request"], raw["request"]); + assert_eq!(changed["event_id"], raw["event_id"]); + assert!(rename_request(&raw.to_string(), new, old).is_err()); + assert!(rename_request(&raw.to_string(), old, "invalid").is_err()); + } +} diff --git a/pubky-marketplace b/pubky-marketplace index 1e47eff..6b73d9d 100755 --- a/pubky-marketplace +++ b/pubky-marketplace @@ -200,7 +200,7 @@ build() { # driver's (a service context). Build both with the classic builder from generated Dockerfiles: the named contexts become # COPYs from .marketplace/sources, the cache mounts go, and the labels name the pinned revisions as the Compose build does. classic_build() { - docker image inspect bitkit-docker/pubky-testnet:0.14.0 >/dev/null 2>&1 || compose build pubky-testnet + docker image inspect bitkit-docker/pubky-testnet:0.15.0 >/dev/null 2>&1 || compose build pubky-testnet printf '**/.git\n**/target\n' > "$SOURCES/.dockerignore" python3 - "$SOURCES/paykit-server/Dockerfile.local" "$PAYKIT_SERVER_REV" "$PAYKIT_RS_REV" "$PAYKIT_SERVER_PATCHES" > "$SOURCES/Dockerfile.classic" <<'PY' import re, sys @@ -219,15 +219,15 @@ if named != 1 or server_copy != 1 or "--mount" in text: print(text + f'\nLABEL tech.masivo.paykit-server="{server}" tech.masivo.paykit-rs="{paykit}" tech.masivo.paykit-server-patches="{patches}"') PY docker build -q -f "$SOURCES/Dockerfile.classic" -t "$PAYKIT_SERVER_IMAGE" "$SOURCES" - { sed -e "s|^COPY --from=paykit |COPY --from=$PAYKIT_SERVER_IMAGE |" -e "s|^COPY --from=fixture |COPY --from=bitkit-docker/payment-request-fixture:rc65-shared |" marketplace/driver/Dockerfile + { sed -e "s|^COPY --from=paykit |COPY --from=$PAYKIT_SERVER_IMAGE |" -e "s|^COPY --from=fixture |COPY --from=bitkit-docker/payment-request-fixture:rc74-shared |" marketplace/driver/Dockerfile echo "LABEL tech.masivo.paykit-server=\"$PAYKIT_SERVER_REV\" tech.masivo.paykit-rs=\"$PAYKIT_RS_REV\""; } > marketplace/driver/.Dockerfile.classic docker build -q -f marketplace/driver/.Dockerfile.classic -t "$DRIVER_IMAGE" marketplace/driver rm -f marketplace/driver/.Dockerfile.classic } images_present() { - docker image inspect bitkit-docker/pubky-testnet:0.14.0 "$PAYKIT_SERVER_IMAGE" \ - "$DRIVER_IMAGE" bitkit-docker/payment-request-fixture:rc65-shared >/dev/null 2>&1 + docker image inspect bitkit-docker/pubky-testnet:0.15.0 "$PAYKIT_SERVER_IMAGE" \ + "$DRIVER_IMAGE" bitkit-docker/payment-request-fixture:rc74-shared >/dev/null 2>&1 } wait_ready() {