Skip to content

Commit 09b5ec0

Browse files
committed
Merge branch 'payjoin-salvage' into pj/07-announcements
2 parents 110a6b2 + 8893f01 commit 09b5ec0

123 files changed

Lines changed: 29852 additions & 908 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/bullock.yml

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ jobs:
7676
# Write-access gating happens twice: the first step fails fast for
7777
# non-write actors, and claude-code-action enforces it again internally —
7878
# this `if` just avoids spinning a runner on unrelated comments.
79-
if: contains(github.event.comment.body, '@bullock')
79+
# The Bot filter matters because GITHUB_TOKEN comments never retrigger
80+
# workflows (GitHub anti-recursion) but GitHub-App comments DO: a claude.yml
81+
# review quoting "@bullock" from the thread would otherwise spin a runner
82+
# and die red at the access gate — noise on the PR, wasted runner.
83+
if: contains(github.event.comment.body, '@bullock') && github.event.comment.user.type != 'Bot'
8084
runs-on: ubuntu-24.04
8185
# analyze_and_test's checks job needs ~30min for setup + `make checks` alone;
8286
# Bullock adds Claude's implement/verify loop on top of the same prefix.
@@ -213,6 +217,13 @@ jobs:
213217
# format, and Write the verdict sentinel.
214218
- name: Bullock (Claude Code)
215219
if: steps.src.outputs.cross != 'true'
220+
# Step-level timeout on purpose: the job-level 60min timeout CANCELS the
221+
# job, and "Act on Bullock's verdict" is guarded by !cancelled() — so a
222+
# job timeout would be a silent red run with no comment. A step timeout
223+
# merely FAILS this step, which degrades to the no-valid-verdict comment.
224+
# Budget: ~6min gate+setup before this step + 45 here + ~2 of git/PR
225+
# plumbing after stays under the job's 60.
226+
timeout-minutes: 45
216227
uses: anthropics/claude-code-action@v1
217228
with:
218229
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -272,6 +283,7 @@ jobs:
272283
273284
## Step 4 — Implement (ONLY if sufficient)
274285
- Change ONLY what was requested. No opportunistic refactors, no unrelated cleanup.
286+
- NEVER modify anything under `.github/` (workflows, actions, CI config) — the workflow refuses to commit such changes. If the request requires a CI change, treat it as insufficient and say so in "question".
275287
- Follow AGENTS.md strictly: use `fvm`; respect the layer/facade/failure/naming rules; NEVER log or expose secrets (mnemonic/seed/xpriv/PIN); no hardcoded user-facing strings (use `context.loc.*`); no raw colors.
276288
- After editing, format your new/changed Dart files so the CI format gate can't fail on untracked files:
277289
run `fvm dart format` on the files you touched, then `git add -A` for the changed source (do NOT add `.bullock/`).
@@ -319,7 +331,10 @@ jobs:
319331
320332
if [ "$sufficient" != "true" ]; then
321333
body="🐂 Bullock needs more info before implementing:"
322-
body="${body}"$'\n\n'"> ${question:-Please clarify what you'd like changed.}"
334+
# NB: no apostrophes inside ${var:-word} — bash treats a single
335+
# quote there as a quoting char even inside double quotes, which
336+
# made this whole script unparseable (exit 2 at EOF).
337+
body="${body}"$'\n\n'"> ${question:-Please clarify what you would like changed.}"
323338
comment "$body"
324339
echo "Insufficient info — asked for clarification, no PR opened."
325340
exit 0
@@ -329,22 +344,27 @@ jobs:
329344
git config user.name "bullock[bot]"
330345
git config user.email "bullock@users.noreply.github.com"
331346
332-
# GITHUB_TOKEN cannot push changes under .github/workflows/ — the push
333-
# (below) would be rejected AFTER commit, going red with no feedback.
334-
# Bail early with an explanation. This is also a deliberate backstop
335-
# against Bullock modifying its own workflow. `git status --porcelain`
336-
# reports both tracked edits and new untracked files, and is checked
337-
# before staging so a workflow-file change never enters the commit.
338-
if [ -n "$(git status --porcelain -- .github/workflows)" ]; then
339-
comment "🐂 Bullock's changes touch \`.github/workflows/\`, which a bot token isn't allowed to push. Please make workflow changes manually."
340-
echo "Workflow files touched — declined (GITHUB_TOKEN cannot push workflows)."
347+
# ALL of .github/ is off-limits to Bullock, for two distinct reasons:
348+
# - .github/workflows/: GITHUB_TOKEN cannot push these — the push
349+
# (below) would be rejected AFTER commit, going red with no feedback.
350+
# - the rest (.github/actions/ composite, CI config): the token CAN
351+
# push them, but they execute in future CI runs with secrets — a
352+
# prompt-injected change here must be blocked mechanically, not just
353+
# by the prompt's SECURITY section. Bullock has no legitimate reason
354+
# to touch CI; a maintainer changes it manually.
355+
# `git status --porcelain` reports both tracked edits and new
356+
# untracked files, and is checked before staging so a CI-file change
357+
# never enters the commit.
358+
if [ -n "$(git status --porcelain -- .github)" ]; then
359+
comment "🐂 Bullock's changes touch \`.github/\` (workflows or CI config), which Bullock isn't allowed to modify. Please make CI changes manually."
360+
echo "CI files touched — declined (.github/ is off-limits to Bullock)."
341361
exit 0
342362
fi
343363
344364
# Stage only source changes; never sweep the whole tree (build_runner
345365
# drift, make side effects, stray artifacts) into the stacked PR, and
346-
# never stage workflow files (guarded above).
347-
git add -A -- ':(exclude).github/workflows'
366+
# never stage CI files (guarded above).
367+
git add -A -- ':(exclude).github'
348368
349369
if git diff --cached --quiet; then
350370
comment "🐂 Bullock judged the request actionable but produced no changes. Please re-summon with a more specific instruction."

Containerfile.tools

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,21 @@ RUN fvm install ${FLUTTER_VERSION} --verbose --no-setup
161161
RUN fvm global ${FLUTTER_VERSION}
162162
ENV PATH="/home/$USER/fvm/default/bin:${PATH}"
163163

164+
164165
# Download Android cmdline-tools
165-
RUN sudo wget -q https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}_latest.zip -O /tmp/android-cmdline-tools.zip
166+
# (USER root instead of sudo: setuid binaries are unreliable under rootless
167+
# podman user namespaces — sudo fails with "account validation failure" —
168+
# and are unnecessary at build time since USER switching achieves the same.)
169+
USER root
170+
RUN wget -q https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}_latest.zip -O /tmp/android-cmdline-tools.zip
166171

167172
# Set up Android SDK
168-
RUN sudo mkdir -p ${ANDROID_HOME}/cmdline-tools
169-
RUN sudo unzip -q /tmp/android-cmdline-tools.zip -d ${ANDROID_HOME}/cmdline-tools
170-
RUN sudo mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest
171-
RUN sudo rm /tmp/android-cmdline-tools.zip
172-
RUN sudo chown -R $USER ${ANDROID_HOME}
173+
RUN mkdir -p ${ANDROID_HOME}/cmdline-tools
174+
RUN unzip -q /tmp/android-cmdline-tools.zip -d ${ANDROID_HOME}/cmdline-tools
175+
RUN mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest
176+
RUN rm /tmp/android-cmdline-tools.zip
177+
RUN chown -R $USER ${ANDROID_HOME}
178+
USER $USER
173179

174180
# Install Android SDK components
175181
RUN yes | sdkmanager --sdk_root=${ANDROID_HOME} --licenses

FEATURES.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ graph TB
4646
BUY[Buy]
4747
COINS[Coins / UTXOs]
4848
ANNOUNCEMENTS[Announcements]
49+
CONSOLIDATION[Consolidation]
4950
5051
%% Dependencies to Core (all features depend on Core, but showing it explicitly would clutter the diagram)
5152
%% Instead, we note this in the documentation below
@@ -84,6 +85,7 @@ graph TB
8485
RECIPIENTS --> EXCHANGE
8586
SECRETS --> CORE
8687
SELL --> EXCHANGE
88+
SEND --> CONSOLIDATION
8789
SEND --> FEES
8890
SEND --> NETWORK
8991
SEND --> PAYJOIN
@@ -94,13 +96,15 @@ graph TB
9496
SETTINGS --> CORE
9597
SWAPS --> UTXO_MGMT
9698
TOR --> CORE
99+
TRANSFER --> CONSOLIDATION
97100
TRANSFER --> SEND
98101
TRANSFER --> RECEIVE
99102
TX_HISTORY --> PAYJOIN
100103
TX_HISTORY --> WALLETS
101104
UTXO_MGMT --> LABELS
102105
UTXO_MGMT --> WALLETS
103106
WALLETS --> BIP85
107+
WALLETS --> CONSOLIDATION
104108
WALLETS --> HW_WALLETS
105109
WALLETS --> NETWORK
106110
WALLETS --> SECRETS
@@ -112,7 +116,7 @@ graph TB
112116
classDef featureStyle fill:#1a202c,stroke:#2d3748,stroke-width:2px,color:#e2e8f0
113117
114118
class CORE coreStyle
115-
class SETTINGS,TOR,PIN_CODE,LABELS,SECRETS,HW_WALLETS,BTC_PRICE,NETWORK,BIP85,FEES,WALLETS,EXCHANGE,APP_STARTUP,UTXO_MGMT,ADDRESS_MGMT,RECIPIENTS,FUNDING,BACKUPS,SWAPS,PAYJOIN,WITHDRAWAL,STATUS,SEND,RECEIVE,TRANSFER,TX_HISTORY,BG_TASKS,AUTOSWAPS,DCA,SELL,PAY,BUY,COINS,ANNOUNCEMENTS featureStyle
119+
class SETTINGS,TOR,PIN_CODE,LABELS,SECRETS,HW_WALLETS,BTC_PRICE,NETWORK,BIP85,FEES,WALLETS,EXCHANGE,APP_STARTUP,UTXO_MGMT,ADDRESS_MGMT,RECIPIENTS,FUNDING,BACKUPS,SWAPS,PAYJOIN,WITHDRAWAL,STATUS,SEND,RECEIVE,TRANSFER,TX_HISTORY,BG_TASKS,AUTOSWAPS,DCA,SELL,PAY,BUY,COINS,ANNOUNCEMENTS,CONSOLIDATION featureStyle
116120
```
117121

118122
## About Package Dependency Diagrams

ios/Runner.xcodeproj/project.pbxproj

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -481,11 +481,11 @@
481481
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
482482
CLANG_ENABLE_MODULES = YES;
483483
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
484-
CURRENT_PROJECT_VERSION = 191;
484+
CURRENT_PROJECT_VERSION = 200;
485485
DEVELOPMENT_TEAM = BX99T32YGS;
486486
ENABLE_BITCODE = NO;
487-
FLUTTER_BUILD_NAME = 6.12.0;
488-
FLUTTER_BUILD_NUMBER = 191;
487+
FLUTTER_BUILD_NAME = 6.13.0;
488+
FLUTTER_BUILD_NUMBER = 200;
489489
INFOPLIST_FILE = Runner/Info.plist;
490490
INFOPLIST_KEY_CFBundleDisplayName = BULL;
491491
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
@@ -494,7 +494,7 @@
494494
"$(inherited)",
495495
"@executable_path/Frameworks",
496496
);
497-
MARKETING_VERSION = 6.12.0;
497+
MARKETING_VERSION = 6.13.0;
498498
PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.app;
499499
PRODUCT_NAME = "$(TARGET_NAME)";
500500
STRIP_STYLE = "non-global";
@@ -674,11 +674,11 @@
674674
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
675675
CLANG_ENABLE_MODULES = YES;
676676
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
677-
CURRENT_PROJECT_VERSION = 191;
677+
CURRENT_PROJECT_VERSION = 200;
678678
DEVELOPMENT_TEAM = BX99T32YGS;
679679
ENABLE_BITCODE = NO;
680-
FLUTTER_BUILD_NAME = 6.12.0;
681-
FLUTTER_BUILD_NUMBER = 191;
680+
FLUTTER_BUILD_NAME = 6.13.0;
681+
FLUTTER_BUILD_NUMBER = 200;
682682
INFOPLIST_FILE = Runner/Info.plist;
683683
INFOPLIST_KEY_CFBundleDisplayName = BULL;
684684
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
@@ -687,7 +687,7 @@
687687
"$(inherited)",
688688
"@executable_path/Frameworks",
689689
);
690-
MARKETING_VERSION = 6.12.0;
690+
MARKETING_VERSION = 6.13.0;
691691
PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.app;
692692
PRODUCT_NAME = "$(TARGET_NAME)";
693693
STRIP_STYLE = "non-global";
@@ -705,11 +705,11 @@
705705
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
706706
CLANG_ENABLE_MODULES = YES;
707707
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
708-
CURRENT_PROJECT_VERSION = 191;
708+
CURRENT_PROJECT_VERSION = 200;
709709
DEVELOPMENT_TEAM = BX99T32YGS;
710710
ENABLE_BITCODE = NO;
711-
FLUTTER_BUILD_NAME = 6.12.0;
712-
FLUTTER_BUILD_NUMBER = 191;
711+
FLUTTER_BUILD_NAME = 6.13.0;
712+
FLUTTER_BUILD_NUMBER = 200;
713713
INFOPLIST_FILE = Runner/Info.plist;
714714
INFOPLIST_KEY_CFBundleDisplayName = BULL;
715715
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
@@ -718,7 +718,7 @@
718718
"$(inherited)",
719719
"@executable_path/Frameworks",
720720
);
721-
MARKETING_VERSION = 6.12.0;
721+
MARKETING_VERSION = 6.13.0;
722722
PRODUCT_BUNDLE_IDENTIFIER = com.bullbitcoin.app;
723723
PRODUCT_NAME = "$(TARGET_NAME)";
724724
STRIP_STYLE = "non-global";

ios/Runner/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
<string>We need access to Bluetooth in order to connect to your hardware wallet when needed</string>
6060
<key>NSLocationWhenInUseUsageDescription</key>
6161
<string>The app itself never collects your location. Location access is only requested if a website opened in the in-app browser asks for it</string>
62+
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
63+
<string>The app itself never collects your location. Location access is only requested if a website opened in the in-app browser asks for it</string>
6264
<key>UIApplicationSupportsIndirectInputEvents</key>
6365
<true/>
6466
<key>UIDesignRequiresCompatibility</key>

lib/core/payjoin/data/datasources/local_payjoin_datasource.dart

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,15 @@ class LocalPayjoinDatasource {
5858
Expression<bool> expr = const Constant(true); // identity
5959

6060
if (onlyUnfinished) {
61+
// isAborted is a terminal outcome too (we already broadcast the
62+
// original in its place) — excluded here for the same reason
63+
// isCompleted/isExpired are, otherwise an aborted session would
64+
// keep being "resumed" on every app start.
6165
expr =
62-
expr & row.isExpired.equals(false) & row.isCompleted.equals(false);
66+
expr &
67+
row.isExpired.equals(false) &
68+
row.isCompleted.equals(false) &
69+
row.isAborted.equals(false);
6370
}
6471

6572
if (walletId != null) {
@@ -78,7 +85,10 @@ class LocalPayjoinDatasource {
7885

7986
if (onlyUnfinished) {
8087
expr =
81-
expr & row.isExpired.equals(false) & row.isCompleted.equals(false);
88+
expr &
89+
row.isExpired.equals(false) &
90+
row.isCompleted.equals(false) &
91+
row.isAborted.equals(false);
8292
}
8393

8494
if (walletId != null) {
@@ -103,10 +113,24 @@ class LocalPayjoinDatasource {
103113
];
104114
}
105115

116+
/// Fetches the payjoin session(s) a transaction id belongs to, matching
117+
/// BOTH the payjoin transaction id and the original transaction id. The
118+
/// original matters as much as the payjoin one: an aborted session (we
119+
/// broadcast the original instead of completing a real payjoin — see
120+
/// PayjoinStatus.aborted) has no [txId] at all, so the transaction that
121+
/// actually hit the chain IS the original — matching only [txId] made
122+
/// that transaction's details lose its payjoin context entirely, hiding
123+
/// the very "aborted" outcome the status exists to communicate. The
124+
/// transactions LIST already joins on both ids
125+
/// (GetTransactionsUsecase); this keeps the details path consistent.
106126
Future<List<PayjoinModel>> fetchByTxId(String txId) async {
107127
final (receivers, senders) = await (
108-
_db.managers.payjoinReceivers.filter((f) => f.txId(txId)).get(),
109-
_db.managers.payjoinSenders.filter((f) => f.txId(txId)).get(),
128+
_db.managers.payjoinReceivers
129+
.filter((f) => f.txId(txId) | f.originalTxId(txId))
130+
.get(),
131+
_db.managers.payjoinSenders
132+
.filter((f) => f.txId(txId) | f.originalTxId(txId))
133+
.get(),
110134
).wait;
111135

112136
return [
@@ -124,6 +148,7 @@ class LocalPayjoinDatasource {
124148
receivers = await receiversTable
125149
.filter((f) => f.isExpired(false))
126150
.filter((f) => f.isCompleted(false))
151+
.filter((f) => f.isAborted(false))
127152
.get();
128153
} else {
129154
receivers = await receiversTable.get();
@@ -147,6 +172,7 @@ class LocalPayjoinDatasource {
147172
senders = await sendersTable
148173
.filter((f) => f.isExpired(false))
149174
.filter((f) => f.isCompleted(false))
175+
.filter((f) => f.isAborted(false))
150176
.get();
151177
} else {
152178
senders = await sendersTable.get();

0 commit comments

Comments
 (0)