Repository navigation
Expand file tree
/
Copy pathpubky-marketplace
More file actions
executable file
·352 lines (325 loc) · 18.3 KB
/
Copy pathpubky-marketplace
File metadata and controls
executable file
·352 lines (325 loc) · 18.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
#!/usr/bin/env bash
# Pubky marketplace test fixture for Bitkit's marketplace wallet journey.
# Docs: docs/pubky-marketplace.md and the "Pubky marketplace fixture" section of README.md.
set -euo pipefail
CLI_NAME="$(basename "$0")"
cd "$(dirname "$0")"
# Pinned upstream revisions. Paykit Server is pubky/paykit-server#46 (head 0ffd4da, paykit-rs v0.1.0-rc65, the version both apps pin; not merged
# or released yet, the latest release is rc8): move PAYKIT_SERVER_REV to its merge or release once #46 lands. Its setup flow emits the Pubky
# grant auth URL (pubkyauth://signin_grant with cid and cpk) that the apps' Paykit SDK requires.
# The Pubky testnet is pinned in marketplace/pubky-testnet/Dockerfile.
PAYKIT_SERVER_REV=${PAYKIT_SERVER_REV:-0ffd4da2adaab048939e0ea18ff916a27a7cfb0e}
PAYKIT_RS_REV=${PAYKIT_RS_REV:-7185ae7da9315028e5331442d71d739c27f1442c} # v0.1.0-rc65, paykit-server's paykit-lib and paykit-sdk pin
PAYKIT_RS_TAG=${PAYKIT_RS_TAG:-v0.1.0-rc65}
LOCKS_REV=${LOCKS_REV:-b3dc87c961f6b907d76cde9c4a28f00670d5231e} # v0.1.0-rc8, paykit-server's locks-core pin
LOCKS_TAG=${LOCKS_TAG:-v0.1.0-rc8}
SOURCES=.marketplace/sources
# Compose reads these for the Paykit Server and driver image tags and labels.
export PAYKIT_SERVER_REV PAYKIT_RS_REV PAYKIT_SERVER_TAG="${PAYKIT_SERVER_REV:0:7}"
# The driver's `paykit-key-authorization` comes from the payment request fixture built on the same paykit-rs (v0.1.0-rc65 -> rc65-shared).
export PAYKIT_FIXTURE_TAG="${PAYKIT_RS_TAG##*-}-shared"
FIXTURE_IMAGE=bitkit-docker/payment-request-fixture:$PAYKIT_FIXTURE_TAG
PAYKIT_SERVER_PATCHES=$(cd marketplace/patches 2>/dev/null && ls paykit-server-*.patch 2>/dev/null | tr '\n' ' ' | sed 's/ $//')
export PAYKIT_SERVER_PATCHES
PAYKIT_SERVER_IMAGE=bitkit-docker/paykit-server:$PAYKIT_SERVER_TAG
DRIVER_IMAGE=bitkit-docker/marketplace-driver:$PAYKIT_SERVER_TAG
PAYKIT_PORT="${MARKETPLACE_PAYKIT_PORT:-23101}"
COMPOSE=(docker compose --profile marketplace)
CHAIN_SERVICES=(bitcoind bitcoinsetup electrs)
FIXTURE_SERVICES=(marketplace-postgres pubky-testnet homeserver-proxy paykit-server)
# Opt-in payment request peers (README, Payment Request fixture). They share the testnet's network namespace.
PEER_SERVICES=(fixture-issuer rc56-peer)
show_help() {
cat <<EOF
Pubky marketplace fixture: Pubky testnet, Paykit Server 0ffd4da (paykit-rs rc65), regtest chain and a purchase driver.
Usage: ${CLI_NAME} <command> [options]
Stack:
build Fetch the pinned sources and build the images
build-paykit Build only Paykit Server and the driver (pins overridable through the environment)
up Build if needed, start the chain and the fixture, wait until ready
ps Show the fixture containers and Paykit Server readiness
logs [service] Follow the fixture logs
down Remove the fixture containers, the payment request peers and state (the chain stays)
reset down, then up: a fresh testnet, Paykit database and seller (start the peers again)
Fixture:
seed [--buyer headless|none] Mine to maturity, create the seller wallet and identity, complete the
watch-only setup, optionally create a headless buyer
info Public fixture facts: homeserver key, seller, buyer, ports
setup-url [--serial <adb-serial>] New watch-only setup auth URL for a Bitkit wallet to approve; --serial
adds -s <serial> to the printed adb command (needed with several devices)
setup-wait <flow> Wait until an approved setup flow completes; with a Bitkit seller
recorded, check the same wallet approved it
seller-auth [--relay <url>] [--timeout <seconds>] [--serial <adb-serial>]
Bitkit seller: print the marketplace write grant (/pub/locks.app/) for the
seller wallet to approve, with the Android and iOS handoff, wait for it
and keep the grant session for purchase --seller bitkit. Prints one JSON
object per line: awaiting_approval first, approved last
fund <address> [sats] Send regtest coins (default 1000000 sats) and mine one block
peers [--seller headless|bitkit|<pubky>] [--buyer <pubky>] [--bundle <id>] [--wait <seconds>]
Report the seller and buyer as linked peers: receiver markers, setup
authority and Paykit Server's link state (linked is true once connected)
Purchase:
purchase [--sats N] [--buyer headless|<pubky>] [--seller headless|bitkit|<pubky>]
Create one purchase; print bundle id, address and delivery state. A Bitkit
seller (after seller-auth and setup) is paid at its wallet's derived address
receive <bundle> Headless buyer: receive the Payment Request and validate it
pay <bundle> Headless buyer: pay the derived address from the regtest wallet
status <bundle> Signed Paykit status and purchase state
mine [--bundle <id>] [--address <bcrt1...>]
Mine exactly one block; print height, hash and the purchase transaction.
--address names the payout address of a Bitkit seller's purchase
wait <bundle> <detected|confirmed> [seconds]
Wait for a signed Paykit status
verify Whole headless journey with assertions; writes .marketplace/evidence/
verify-bitkit-seller Bitkit seller path with a headless stand-in for the wallet (both approvals,
purchase, payout to the stand-in xpub address); writes .marketplace/evidence/
EOF
}
compose() { "${COMPOSE[@]}" "$@"; }
# Progress goes to stderr, so stdout stays the command's own output.
progress() { echo "[${CLI_NAME}] $*" >&2; }
# The driver image takes the helper binaries from the paykit-server image through `additional_contexts:
# paykit: service:paykit-server`, so every compose command that may build or run the driver prepares the
# paykit-server build context, and compose stops when .marketplace/sources is missing, even with all the images
# present (a fresh clone on a host that built before). Fetch the pinned trees whenever one is missing. This is
# quiet and offline when they are there; keeping the driver's link to that context stays the simpler wiring than
# feeding it the helpers by another route, which would tie the driver image to an image name.
# A tree at another revision (left by a build with other pins) is checked out again, so compose never builds an image tagged for
# these pins from it.
ensure_sources() {
if [ ! -f "$SOURCES/paykit-server/Dockerfile.local" ] || [ ! -d "$SOURCES/paykit-rs/paykit-lib" ] ||
[ ! -d "$SOURCES/paykit-rs/paykit-sdk" ] || [ ! -d "$SOURCES/locks" ] ||
[ "$(git -C "$SOURCES/paykit-server" rev-parse HEAD)" != "$PAYKIT_SERVER_REV" ] ||
[ "$(git -C "$SOURCES/paykit-rs" rev-parse HEAD)" != "$PAYKIT_RS_REV" ] ||
[ "$(git -C "$SOURCES/locks" rev-parse HEAD)" != "$LOCKS_REV" ]; then
progress "fetching the pinned sources under $SOURCES"
fetch_sources >&2
else
apply_paykit_server_patches >&2 # a patch added since the checkout
fi
}
# The driver runs as root inside its container (it owns the root-only state volume). It hands the files it
# writes to the host mounts (.marketplace/evidence) to the calling user, so the host can remove them later.
driver() {
ensure_sources
mkdir -p .marketplace/evidence
compose run --rm --no-deps --quiet-build -T \
-e MARKETPLACE_HOST_UID="$(id -u)" -e MARKETPLACE_HOST_GID="$(id -g)" marketplace-driver "$@"
}
# Older runs left root-owned evidence that the host user cannot delete. Empty it through the driver image
# (root in the container), then remove the directory itself.
remove_evidence() {
[ -e .marketplace/evidence ] || return 0
if ! rm -rf .marketplace/evidence 2>/dev/null; then
docker run --rm -v "$PWD/.marketplace:/host" --entrypoint find bitkit-docker/marketplace-driver:local \
/host/evidence -mindepth 1 -delete
rm -rf .marketplace/evidence
fi
}
project_name() { docker compose config --format json | jq -r '.name'; }
fetch_pinned() {
local url="$1" dir="$2" rev="$3"
if [ ! -d "$dir/.git" ]; then
mkdir -p "$dir"
git -C "$dir" init -q
git -C "$dir" remote add origin "$url"
fi
if [ "$(git -C "$dir" rev-parse -q --verify HEAD 2>/dev/null || true)" != "$rev" ]; then
git -C "$dir" checkout -q -- . 2>/dev/null || true # drop the fixture's patches before moving to another revision
git -C "$dir" fetch -q --depth 1 origin "$rev"
git -C "$dir" checkout -q --detach FETCH_HEAD
fi
if [ "$(git -C "$dir" rev-parse HEAD)" != "$rev" ]; then
echo "$dir is not at $rev" >&2
exit 1
fi
echo "pinned $dir @ ${rev:0:8}"
}
# The paykit-server tree resolves paykit-rs (by tag, or by rev since rc9) and locks by tag through its Cargo.lock, and its
# Dockerfile.local swaps those for the checkouts below. Fail here, before a long build, if the lock does not resolve to our pins.
check_lock_pins() {
local lock="$SOURCES/paykit-server/Cargo.lock"
if ! grep -Fq -e "source = \"git+https://github.com/pubky/paykit-rs.git?tag=$PAYKIT_RS_TAG#$PAYKIT_RS_REV\"" \
-e "source = \"git+https://github.com/pubky/paykit-rs.git?rev=$PAYKIT_RS_REV#$PAYKIT_RS_REV\"" "$lock" ||
! grep -Fq "source = \"git+https://github.com/pubky/locks.git?tag=$LOCKS_TAG#$LOCKS_REV\"" "$lock"; then
echo "$lock does not lock paykit-rs $PAYKIT_RS_REV and locks $LOCKS_REV" >&2
exit 1
fi
}
# Fixes the fixture carries on top of the pinned Paykit Server tree until upstream has them (marketplace/patches, named in the
# image label tech.masivo.paykit-server-patches). Each applies once; a patch whose `Upstream:` commit the pinned tree already contains
# is skipped; a patch that neither applies nor is applied otherwise stops the build.
apply_paykit_server_patches() {
local patch upstream
for patch in marketplace/patches/paykit-server-*.patch; do
[ -e "$patch" ] || continue
upstream=$(sed -n 's/^Upstream: \([0-9a-f]\{40\}\).*/\1/p' "$patch")
if git -C "$SOURCES/paykit-server" apply --check "$PWD/$patch" 2>/dev/null; then
git -C "$SOURCES/paykit-server" apply "$PWD/$patch"
echo "patched $SOURCES/paykit-server with $(basename "$patch")"
elif git -C "$SOURCES/paykit-server" apply --check --reverse "$PWD/$patch" 2>/dev/null; then
:
elif [ -n "$upstream" ] && { [ ! -f "$SOURCES/paykit-server/.git/shallow" ] ||
git -C "$SOURCES/paykit-server" fetch -q --unshallow origin "$PAYKIT_SERVER_REV"; } &&
git -C "$SOURCES/paykit-server" merge-base --is-ancestor "$upstream" HEAD 2>/dev/null; then
echo "skipped $(basename "$patch"): paykit-server ${PAYKIT_SERVER_REV:0:8} contains its fix ${upstream:0:8}"
else
echo "$patch does not apply to paykit-server ${PAYKIT_SERVER_REV:0:8}; drop it if upstream fixed it" >&2
exit 1
fi
done
}
fetch_sources() {
fetch_pinned https://github.com/pubky/paykit-server.git "$SOURCES/paykit-server" "$PAYKIT_SERVER_REV"
apply_paykit_server_patches
fetch_pinned https://github.com/pubky/paykit-rs.git "$SOURCES/paykit-rs" "$PAYKIT_RS_REV"
fetch_pinned https://github.com/pubky/locks.git "$SOURCES/locks" "$LOCKS_REV"
check_lock_pins
}
# The payment request fixture image on PAYKIT_RS_REV, built when missing (its Dockerfile moves the fixture's paykit-rs and Pubky pins).
ensure_fixture() {
docker image inspect "$FIXTURE_IMAGE" >/dev/null 2>&1 && return 0
progress "building $FIXTURE_IMAGE on paykit-rs ${PAYKIT_RS_REV:0:8}"
if docker buildx version >/dev/null 2>&1; then
docker build -q -f payment-requests/Dockerfile --build-arg "PAYKIT_RS_REV=$PAYKIT_RS_REV" -t "$FIXTURE_IMAGE" payment-requests
else
sed -E -e 's/^RUN --mount=[^ ]+ \\/RUN \\/' -e '/^[[:space:]]+--mount=/d' payment-requests/Dockerfile |
docker build -q -f - --build-arg "PAYKIT_RS_REV=$PAYKIT_RS_REV" -t "$FIXTURE_IMAGE" payment-requests
fi
}
# Paykit Server and the driver, which carries its helpers and the payment request fixture's `paykit-key-authorization` (scripts/follow-app-paykit sets the pins through the environment).
build_paykit() {
fetch_sources
ensure_fixture
if docker buildx version >/dev/null 2>&1; then
compose build paykit-server
compose build marketplace-driver
else
classic_build
fi
}
build() {
fetch_sources
# the payment request fixture's image first: the driver copies its `paykit-key-authorization` from it
compose build homeserver-proxy
ensure_fixture
if docker buildx version >/dev/null 2>&1; then
compose build pubky-testnet paykit-server
compose build marketplace-driver
else
classic_build
fi
}
# Docker without BuildKit (the QA boxes) cannot build Paykit Server's Dockerfile.local (named contexts, cache mounts) or the
# 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
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
text, server, paykit, patches = open(sys.argv[1]).read(), sys.argv[2], sys.argv[3], sys.argv[4]
text = re.sub(r"^# syntax=.*\n", "", text)
copies = "\n".join([
"COPY paykit-rs/paykit-lib /build/paykit-rs/paykit-lib",
"COPY paykit-rs/paykit-sdk /build/paykit-rs/paykit-sdk",
"COPY locks/Cargo.toml locks/Cargo.lock /build/locks/",
] + [f"COPY locks/{crate} /build/locks/{crate}" for crate in ("locks-core", "locks-service", "locks-server", "locks-sdk", "locks-e2e")])
text, named = re.subn(r"RUN --mount=from=paykit-lib.*?/build/locks/\n", copies + "\n", text, flags=re.S)
text, server_copy = re.subn(r"^COPY \. /build/paykit-server$", "COPY paykit-server /build/paykit-server", text, flags=re.M)
text = re.sub(r"RUN --mount=type=cache[^\n]*\\\n(\s+--mount=type=cache[^\n]*\\\n)*", "RUN ", text)
if named != 1 or server_copy != 1 or "--mount" in text:
sys.exit("Dockerfile.local changed shape; update classic_build")
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=$FIXTURE_IMAGE |" 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 "$FIXTURE_IMAGE" >/dev/null 2>&1
}
wait_ready() {
local deadline=$(($(date +%s) + ${1:-300}))
until [ "$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PAYKIT_PORT}/health/ready" || true)" = "200" ]; do
if [ "$(date +%s)" -gt "$deadline" ]; then
echo "Paykit Server did not become ready; see: ${CLI_NAME} logs paykit-server" >&2
exit 1
fi
sleep 2
done
curl -fsS "http://127.0.0.1:${PAYKIT_PORT}/health/ready"
echo
}
up() {
mkdir -p .marketplace/evidence
ensure_sources
if ! images_present; then
progress "building the images"
build
fi
progress "starting the chain, Postgres and the Pubky testnet"
compose up -d "${CHAIN_SERVICES[@]}" marketplace-postgres pubky-testnet homeserver-proxy
progress "initializing the driver state"
driver init
progress "starting Paykit Server"
compose up -d paykit-server
progress "waiting for Paykit Server to be ready"
wait_ready
echo "Electrum: tcp://127.0.0.1:60001"
echo "Next: ${CLI_NAME} seed"
}
# The Pubky testnet keeps its homeserver files and DHT in memory, so it cannot
# restart with its accounts intact. The fixture is disposable as a whole.
down() {
local project
project="$(project_name)"
progress "removing the fixture and payment request containers"
# The peers run in the testnet's namespace and hold identities from it, so they go with it. Their profile is
# only named here so that rm reaches them; it does nothing when they were never created.
compose --profile payment-requests rm -sfv "${PEER_SERVICES[@]}" "${FIXTURE_SERVICES[@]}" marketplace-driver
progress "removing the Postgres and state volumes"
docker volume rm -f "${project}_marketplace_postgres_data" "${project}_marketplace_state" >/dev/null
progress "removing the evidence"
remove_evidence
}
if [ $# -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
show_help
exit 0
fi
command="$1"
shift
# Without buildx, compose falls back to the classic builder but says so on every run. That line lands on stderr
# and breaks `2>&1 | jq`. Set COMPOSE_BAKE yourself to keep it as it is.
if [ -z "${COMPOSE_BAKE+x}" ] && ! docker buildx version >/dev/null 2>&1; then
export COMPOSE_BAKE=false
fi
case "$command" in
build) build ;;
build-paykit) build_paykit ;;
up) up ;;
ps)
compose ps "${CHAIN_SERVICES[@]}" "${FIXTURE_SERVICES[@]}" || true
curl -s "http://127.0.0.1:${PAYKIT_PORT}/health/ready" || echo "Paykit Server: not reachable"
echo
;;
logs) compose logs -f --tail 100 "${@:-paykit-server}" ;;
down) down ;;
reset)
progress "reset: down, then up (a first build or a slow host takes minutes)"
down
up
progress "reset: done; seed next"
;;
seed | info | setup-url | setup-wait | seller-auth | fund | peers | purchase | receive | pay | status | mine | wait | verify | verify-bitkit-seller)
driver "$command" "$@"
;;
*)
echo "Unknown command: $command" >&2
show_help >&2
exit 1
;;
esac