Skip to content

Commit 834ec97

Browse files
committed
build(payjoin): pin reproducibility fix
1 parent 53e25fc commit 834ec97

4 files changed

Lines changed: 172 additions & 6 deletions

File tree

makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: all setup clean deps deps-update bootstrap analyze build-runner translations hooks ios-pod-update ios-release drift-migrations devcontainer devcontainer-up container-tools container-app android release debug beta verify verify-rustc-pins test unit-test integration-test catalogue fvm-check
1+
.PHONY: all setup clean deps deps-update prepare-payjoin-dependency bootstrap analyze build-runner translations hooks ios-pod-update ios-release drift-migrations devcontainer devcontainer-up container-tools container-app android release debug beta verify verify-rustc-pins test unit-test integration-test catalogue fvm-check
22

33
fvm-check:
44
@echo "🔍 Checking FVM"
@@ -23,6 +23,12 @@ clean:
2323
deps:
2424
@echo "🏃 Fetch dependencies (enforce pubspec.lock)"
2525
@fvm flutter pub get --enforce-lockfile
26+
@fvm dart tools/prepare_payjoin_dependency.dart
27+
28+
# Git checkouts omit the generated binding; the published archive is the SHA-pinned oracle.
29+
# Use `dart` rather than `dart run` intentionally: prepare the source before native build hooks run.
30+
prepare-payjoin-dependency:
31+
@fvm dart tools/prepare_payjoin_dependency.dart
2632

2733
# Intentionally re-resolve from scratch: deletes the lockfiles and lets pub pick
2834
# fresh versions (and, for branch refs, fresh commits). Use only when you mean to
@@ -31,6 +37,7 @@ deps-update:
3137
@echo "🔓 Re-resolving dependencies (deletes pubspec.lock + ios/Podfile.lock)"
3238
@rm -f pubspec.lock ios/Podfile.lock
3339
@fvm flutter pub get
40+
@fvm dart tools/prepare_payjoin_dependency.dart
3441

3542
# Melos workspace bootstrap (pub get across the workspace + package linking).
3643
# Wraps `fvm dart run melos` so the pinned SDK (.fvmrc) is used — never bare

packages/bull_payjoin/pubspec.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ dependencies:
1919
drift: ^2.29.0
2020
freezed_annotation: ^3.1.0
2121
meta: ^1.17.0
22-
payjoin: ^0.2.1+payjoin-1.0.0-rc.8
22+
payjoin:
23+
git:
24+
url: https://github.com/SatoshiPortal/payjoin-rust
25+
ref: 9e760b537175da70150e8a58bddb4d333e523aa4
26+
path: payjoin-ffi/dart
2327
primitives:
2428
sqlite3: ^2.9.4
2529
synchronized: ^3.4.0

pubspec.lock

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1496,10 +1496,11 @@ packages:
14961496
payjoin:
14971497
dependency: transitive
14981498
description:
1499-
name: payjoin
1500-
sha256: "52696fe34ef3c05f9827f5dd08a984e8ea86642677ba934f65a7f3a722755985"
1501-
url: "https://pub.dev"
1502-
source: hosted
1499+
path: "payjoin-ffi/dart"
1500+
ref: "9e760b537175da70150e8a58bddb4d333e523aa4"
1501+
resolved-ref: "9e760b537175da70150e8a58bddb4d333e523aa4"
1502+
url: "https://github.com/SatoshiPortal/payjoin-rust"
1503+
source: git
15031504
version: "0.2.1+payjoin-1.0.0-rc.8"
15041505
permission_handler:
15051506
dependency: "direct main"
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
import 'package:crypto/crypto.dart';
5+
6+
const _archiveUrl =
7+
'https://pub.dev/api/archives/payjoin-0.2.1%2Bpayjoin-1.0.0-rc.8.tar.gz';
8+
const _archiveSha256 =
9+
'52696fe34ef3c05f9827f5dd08a984e8ea86642677ba934f65a7f3a722755985';
10+
const _bindingSha256 =
11+
'aee74c23bf5076c5db8dda79c9ac1866446d3c4e237dbafeddd52c3efe499500';
12+
13+
Future<void> main() async {
14+
Directory? temporaryDirectory;
15+
File? temporaryBinding;
16+
17+
try {
18+
final packageRoot = _findPayjoinRoot();
19+
final binding = File('${packageRoot.path}/lib/payjoin.dart');
20+
21+
if (binding.existsSync()) {
22+
final actualHash = await _sha256(binding);
23+
if (actualHash != _bindingSha256) {
24+
throw StateError(
25+
'Existing payjoin binding has SHA-256 $actualHash, expected '
26+
'$_bindingSha256.',
27+
);
28+
}
29+
stdout.writeln('Validated existing payjoin binding: ${binding.path}');
30+
return;
31+
}
32+
33+
stdout.writeln('Payjoin binding is missing; hydrating ${binding.path}');
34+
temporaryDirectory = await Directory.systemTemp.createTemp('payjoin-');
35+
final archive = File('${temporaryDirectory.path}/payjoin.tar.gz');
36+
await _downloadArchive(archive);
37+
final archiveHash = await _sha256(archive);
38+
if (archiveHash != _archiveSha256) {
39+
throw StateError(
40+
'Downloaded payjoin archive has SHA-256 $archiveHash, expected '
41+
'$_archiveSha256.',
42+
);
43+
}
44+
45+
final extractionDirectory = await Directory(
46+
'${temporaryDirectory.path}/extract',
47+
).create();
48+
final tarResult = await Process.run('tar', [
49+
'-xzf',
50+
archive.path,
51+
'-C',
52+
extractionDirectory.path,
53+
'--strip-components=1',
54+
'lib/payjoin.dart',
55+
]);
56+
if (tarResult.exitCode != 0) {
57+
throw StateError(
58+
'Could not extract lib/payjoin.dart from the verified payjoin archive: '
59+
'${tarResult.stderr}'
60+
.trim(),
61+
);
62+
}
63+
64+
final extractedBinding = File('${extractionDirectory.path}/payjoin.dart');
65+
if (!extractedBinding.existsSync()) {
66+
throw StateError(
67+
'Verified payjoin archive did not contain lib/payjoin.dart.',
68+
);
69+
}
70+
final bindingHash = await _sha256(extractedBinding);
71+
if (bindingHash != _bindingSha256) {
72+
throw StateError(
73+
'Extracted payjoin binding has SHA-256 $bindingHash, expected '
74+
'$_bindingSha256.',
75+
);
76+
}
77+
78+
temporaryBinding = File('${binding.path}.tmp');
79+
if (temporaryBinding.existsSync()) {
80+
throw StateError(
81+
'Refusing to use an existing temporary binding: ${temporaryBinding.path}',
82+
);
83+
}
84+
await extractedBinding.copy(temporaryBinding.path);
85+
await temporaryBinding.rename(binding.path);
86+
temporaryBinding = null;
87+
stdout.writeln('Installed verified payjoin binding: ${binding.path}');
88+
} catch (error) {
89+
stderr.writeln('Failed to prepare payjoin dependency: $error');
90+
exitCode = 1;
91+
} finally {
92+
if (temporaryBinding != null && temporaryBinding.existsSync()) {
93+
temporaryBinding.deleteSync();
94+
}
95+
temporaryDirectory?.deleteSync(recursive: true);
96+
}
97+
}
98+
99+
Directory _findPayjoinRoot() {
100+
final configFile = File('.dart_tool/package_config.json');
101+
if (!configFile.existsSync()) {
102+
throw StateError(
103+
'Missing .dart_tool/package_config.json; run pub get first.',
104+
);
105+
}
106+
107+
final config = jsonDecode(configFile.readAsStringSync());
108+
final packages = config is Map<String, dynamic> ? config['packages'] : null;
109+
if (packages is! List) {
110+
throw StateError(
111+
'Invalid .dart_tool/package_config.json: packages is missing.',
112+
);
113+
}
114+
115+
for (final package in packages) {
116+
if (package is! Map<String, dynamic> || package['name'] != 'payjoin') {
117+
continue;
118+
}
119+
final rootUri = package['rootUri'];
120+
if (rootUri is! String) {
121+
throw StateError(
122+
'Invalid payjoin package configuration: rootUri is missing.',
123+
);
124+
}
125+
final configUri = configFile.absolute.uri;
126+
final root = Uri.parse(rootUri).isAbsolute
127+
? Uri.parse(rootUri)
128+
: configUri.resolve(rootUri);
129+
return Directory.fromUri(root);
130+
}
131+
132+
throw StateError('Package "payjoin" was not found in package_config.json.');
133+
}
134+
135+
Future<void> _downloadArchive(File destination) async {
136+
final client = HttpClient();
137+
try {
138+
final request = await client.getUrl(Uri.parse(_archiveUrl));
139+
request.followRedirects = true;
140+
request.maxRedirects = 5;
141+
final response = await request.close();
142+
if (response.statusCode != HttpStatus.ok) {
143+
throw StateError(
144+
'pub.dev returned HTTP ${response.statusCode} for the payjoin archive.',
145+
);
146+
}
147+
await response.pipe(destination.openWrite());
148+
} finally {
149+
client.close(force: true);
150+
}
151+
}
152+
153+
Future<String> _sha256(File file) async =>
154+
sha256.convert(await file.readAsBytes()).toString();

0 commit comments

Comments
 (0)