Repository navigation
fix(pay): Circle の経路で、タブの対応を保存できない端末では新しい支払いを始めない - #1093
Merged
cipherwebllc merged 7 commits intoNov 13, 2026
Merged
Conversation
Circle の経路 (USDC ガスレス) は、同じタブの続きを sessionStorage の対応 (chain・sender・内容から attemptId) で見分ける。 これまで lib/circlePending.ts の stableAttemptId は、sessionStorage の getItem や setItem が throw すると、 保存していない新しい UUID を黙って返し、hook はそれで送っていた。その端末で送った支払いが確認待ちで終わると、 次の押下は別の attemptId になり、裏で確定した前の支払い (90 秒より前の confirmed) の後に 2 本目の署名を求める。 確認待ちの文の「このタブでもう一度押すと、まず同じ支払いの結果を確かめ直します」も成り立たなかった。 stableAttemptId は、保存できた attemptId だけを返すようにした (戻り値は string か null)。 既にある値を読めたらそれを返し、書かない。無ければ新しい UUID を書き、読み戻して同じ値のときだけ返す。 getItem か setItem の throw、読み戻しの不一致は null で、warn を 1 行出す (chainId と、どこで失敗したかだけ)。 UUID の採番は try の外に出した (storage と関係ない失敗を null にしない)。 hooks/useBatchPayment.ts の runCirclePayment は、null なら executeCirclePayment を呼ぶ前に止める。 古い記録の掃除、scan、記録の予約、署名、送信のどれも始まっていない。既存の flag (pendingStoreUnavailable) を立て、 PimlicoPendingStoreUnavailableError を投げる。画面には、localStorage を読めない端末と同じ 「この端末では記録を保存できません」の欄が出る。フォーム、lib/paymentFlowGuards、messages は変えていない。 見える動きが変わるのは 2 つ。sessionStorage だけが使えない端末は、署名を求められずに上の欄が出る。 localStorage と sessionStorage の両方で setItem が失敗し getItem は通る端末は、記録の予約の書き込みの失敗の文 (赤い枠) の代わりに上の欄が出る。どちらも送らない。 既にある対応を読めた押下 (確認待ちの続き) と、明示の paymentAttemptId の呼び出しは、今までどおり。 対応の付け替えの書き込みの失敗の扱いも変えていない。lib/smartAccount/circleSend.ts はコメントだけを事実に合わせた。 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc
止める所は executeCirclePayment を包む try の外にある (try の catch は対応を破棄しに行く)。 止まった押下の間に sessionStorage と localStorage の removeItem が 1 回も呼ばれないことを、hook の test で見る。 止める所を try の中へ動かす変異が、これで落ちる。 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
前の commit は、タブの対応 (sessionStorage) を保存できない押下を executeCirclePayment の前で止めていた。 それだと、既にある支払いの復旧まで止まる。送った支払いが確認待ち (記録は submitting) や、裏で確定した直後 (confirmed・90 秒以内) のときに sessionStorage の getItem が throw するようになると、main は scan が記録を拾って 結果を返すのに、前の commit は欄を出して止まり、取り出せる結果を失っていた。 止める所を、executeCirclePayment の中の「scan の loop を抜けた後、reserveOrResume の前」に移した。 stableAttemptId は attemptId と saved を返す。保存できなければ、保存していない新しい UUID と saved の false を返し、 warn を 1 行出す (前の commit と同じ見分け方)。hook は saved を mappingSaved として executeCirclePayment に渡す。 false のとき executeCirclePayment は、新しい記録を予約する直前で CircleAttemptMappingUnavailableError を投げる。 hook はその name を受けて既存の flag を立て、PimlicoPendingStoreUnavailableError を投げる。対応は破棄しない。 scan の中の動きは変えていない。直近の confirmed の結果を返す、submitting を復旧する、確かめられない記録と 直近の pre-submit の確認待ち、古い pre-submit の abandon、入口の掃除は、対応を保存できていない押下でも今までどおり。 付け替えも今までどおり呼び、hook が書けなければ確認待ちは (e) になる。 mappingSaved を省略した呼び出しと true の呼び出し (明示の paymentAttemptId を含む) は今までどおり。 新しい支払いを作る所まで来た押下は、署名を求めずに「この端末では記録を保存できません」の欄になる。 90 秒より前に確定した支払いの記録だけがある押下も同じ (これまでは 2 本目の署名を求めていた)。 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc
sessionStorage の getItem だけが throw し setItem は通る端末では、scan の付け替えの書き込みは通る。 前の commit は、その復旧が確認待ちで終わると場面 (a) を出していた。(a) は「24 時間以内にこのタブで同じ内容のまま もう一度押すと、まず同じ支払いの結果を確かめ直します」と言うが、その端末の次の押下は対応を読めないので、 付け替えた記録の resume にならず、また scan から入る。最後に渡してから 30 分より後は、この約束が成り立たない。 hook の adoptAttempt は、最初の対応を保存できていない押下 (saved が false) では、書けても false を返すようにした。 executeCirclePayment は今までの規則どおり (e) を出す。書き込み自体は今までどおり試す (後で読めるようになった端末では、対応がその記録を指している方がよい)。この試みの対応の指す先を替えるのも、 今までどおり書けたときだけ。executeCirclePayment のコードは変えていない (コメントだけ)。 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc
最初の対応の読み戻しだけが throw する押下 (saved は false・その後は読める) が、別のタブの確定した支払いの結果を 受け取る筋を hook の test に足した。付け替えを書けたら、この試みの対応の指す先は付け替えた先になり、 結果を受け取った所で対応が消える (今までどおり)。hook が circleSend に返す値だけが false になる。 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc
前の commit は、新しい記録を予約する直前で止まった押下で pendingStoreUnavailable の flag を立てていた。 flag が立つとフォームは送信を閉じるので、止まった後に復旧できるようになった支払いを、その画面から受け取れなかった。 例は、最後に渡してから 30 分より後の submitting の記録で、nonce は使用済みだが bundler がまだ receipt を返さないもの。 scan はその記録を飛ばすので止まるが、receipt が引けるようになれば、次の押下の scan が元の支払いの結果を返す。 main は押せば受け取れるので、flag の版は main より悪かった。 flag と PimlicoPendingStoreUnavailableError への変換をやめた (setter の受け渡しも消した)。 CircleAttemptMappingUnavailableError に場面の符号 mapping_unavailable を持たせ、6 つ目の場面 (f) として、 3 つのフォームが赤い枠に CircleErrors の文を出す (lib/circleErrorKey.ts に符号と key を足した。フォームは変えていない)。 送信は閉じない。押すたびに scan が走り、復旧できれば結果を受け取り、できなければ、また新しい支払いの直前で止まる。 hook は、この name では対応を消さない (CirclePendingError などと同じ並びに足した)。 mappingSaved の受け渡しと、付け替えの戻り値 (書けて、かつ保存できているときだけ true) は前の commit のまま。 (f) の文 (messages の CircleErrors.mappingUnavailable の ja と en、circleSend.ts の同じ文) は案で、承認待ち。 stableAttemptId の中の logger.warn を try と catch で包んだ。診断の log の失敗が、既にある支払いの復旧と、 新しい支払いを止める処理を巻き込まないようにするため。 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc
場面 (f) の文 (ja と en) は user が承認した (2026-11-13)。 circleSend.ts と circleErrorKey.test.ts のコメントの「案・承認待ち」を直した。コードと test の期待は変えていない。 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc
cipherwebllc
deleted the
fix/r10-ss-circle-attempt-mapping-unavailable
branch
November 13, 2026 14:58
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
第 10 回レビューの後追い「r10-ss」(money-path)。PR 1091 の Codex 4 回目の P2 2 を「残る限界」として受け入れたときの、後続の PR です (user 裁定 2026-11-13「指摘 2 は A+B」)。設計は Fable、計画レビューは Opus (GO-WITH-CHANGES・反映済み)。
merge は user の明示の承認を待ちます (掟 15)。この PR は公開文言を 1 つ足します (場面 (f) の文・ja / en)。文は ja / en とも user が承認済みです (2026-11-13・下の「公開文言」)。
何が穴だったか
lib/circlePending.tsのstableAttemptIdは、sessionStorage の getItem / setItem が throw すると、保存していない新しい UUID を黙って返していた (呼ぶ側は見分けられない)。コメントは「マッピングは defense-in-depth であり決済可否を左右させない」と言っていたが、H-V 以降、この対応は二重払いを防ぐ本体になっている。変えたこと
lib/circlePending.tsstableAttemptIdの戻り値を{ attemptId: string; saved: boolean }に。既にある値を読めたら、それとsaved: true(書かない)。無ければ新しい UUID を書き、読み戻して同じ値のときだけsaved: true。getItem / setItem の throw・読み戻しの不一致は、保存していない新しい UUID とsaved: false。そのときlogger.warn('circle.attempt_mapping.unavailable', { chainId, cause })(cause は'read' | 'write' | 'readback')。warn は try / catch で包んだ (診断の log の失敗が、既にある支払いの復旧と、新しい支払いを止める処理を巻き込まないため)。UUID の採番は try の外。setStableAttemptId・clearStableAttemptIdは変えていないlib/smartAccount/circleSend.tsexecuteCirclePaymentの引数にmappingSaved?: boolean(省略 = true)。false のとき、scan の loop を抜けた後・reserveOrResumeの直前で、新しい classCircleAttemptMappingUnavailableErrorを投げる (name 同名・circleCode: 'mapping_unavailable'・message は (f) の ja の文)。足したのは、引数 1 つ・class 1 つ・文 1 つ・throw 1 か所。scan の中の動き・ほかの符号・ほかの文は変えていない。冒頭の説明に「対応を保存できていない押下」の段落を足し、(a)・(e) の注記を事実に合わせたlib/circleErrorKey.tsmapping_unavailable→ keymappingUnavailableを足した (6 つ目の場面)。関数は変えていないmessages/ja.json・messages/en.jsonCircleErrorsにmappingUnavailableを足した (承認済み・2026-11-13)hooks/useBatchPayment.tsrunCirclePaymentがsavedをmappingSavedとして渡す (明示の paymentAttemptId の押下には true を渡す)。adoptAttempt(scan の付け替え) は、savedが false の押下では、書けても false を返す (written && saved。書き込み自体は今までどおり試し、「対応の指す先」を替えるのも今までどおり書けたとき)。catch の「対応を消さない name」の並びにCircleAttemptMappingUnavailableErrorを足した。flag (pendingStoreUnavailable) は立てない・error を替えない (v1・v2 で足した setter の受け渡しは消した)。hook に logger は足していない止める所より前に、新しい記録の書き込み・署名・送信は無い (
reserveOrResumeが最初の書き込み)。saved: falseの attemptId は押すたびに変わるので、その key に記録があることは無く、前の押下の記録は scan が「別の key の記録」として拾う。3 フォーム・
lib/paymentFlowGuardsは変えていない。フォームは、PR 1091 の経路 (circleErrorKey) で、符号のある error をCircleErrorsの文にして赤い枠に出す。公開文言 (user が承認済み・2026-11-13)
CircleErrors.mappingUnavailableCircleErrors.mappingUnavailablecircleSend.tsのCIRCLE_ERROR_JA・tests/lib/circleErrorKey.test.ts)。文が変わったら 3 か所と en を一緒に替える (ずれは test が止める)。対応を保存できない押下の結果
見える動きの変化 (main との差・1 つずつ)
PendingStoreWriteError) の文「pending record 書込失敗 (送信を中止): …」が赤い枠に出ていた (英語の画面でも日本語)。payment.failed・/checkout =checkout.failed・チップ =tip.failed)。この端末の履歴の error の行は、/pay と /checkout だけが書く (チップは履歴の hook を通らないので書かない)。同じ error が同じ秒に重なった分は 1 行にまとまり (履歴の id が同じになる)、localStorage に書けない端末では行は残らない。ボタンが押せるままなので、押した回数に応じて増える。 履歴の行の文は ja の固定文。circle.attempt_mapping.unavailable・Sentry の warning)。入るのは chainId と cause だけ。止まらなかった押下 (scan が結果を返した・復旧した) でも出る。stableAttemptIdの戻り値 (string→{ attemptId, saved }・本番の呼び出し元は hook の 1 か所)。ExecuteCirclePaymentArgsにmappingSaved(省略可)。CircleErrorCode・CircleErrorKeyに 1 つずつ。export の class が 1 つ増える。「この端末では記録を保存できません」の欄 (押せなくする flag) は、この PR では出さない。localStorage を読めない端末の既存の欄は今までどおり。
変わらないこと・触っていないこと
saved: true・確認待ちの続き)。その後に書けなくなった端末でも、読めれば自分の key を resume する (書き直さない)。adoptAttempt→setStableAttemptId) を呼ぶ条件・順番・書き込みと、書けなかったときの扱い。executeCirclePayment の規則 (hook の関数が true を返したら (a)・false なら (e)) も同じ。paymentAttemptIdの呼び出し (本番の 3 フォームには無い)。hook はmappingSavedに true を渡すので、止まらない (動きは今までどおり)。PendingStoreWriteErrorの文が赤い枠に出る・test で残した)。lib/paymentFlowGuards。(a)〜(e) の符号と文。テスト
tests/lib/circlePending.test.ts(stableAttemptId): 無い → 書いてsaved: true・setItem が throw / getItem が throw →saved: falseと、押すたびに別の UUID・保存した後に getItem が throw → 保存した値とは別の UUID (保存した値は残る)・silent drop・読み戻すと別の値・読み戻しが throw・既にある値 →saved: true(setItem を呼ばない)・localStorage だけ壊れていても保存できる・warn の中身 (chainId と cause だけ)・warn が throw しても投げない (返す値は同じ)・crypto.randomUUIDの失敗はsaved: falseにしない。tests/lib/circleErrorKey.test.ts: 符号 (f) → key・本物の class (name・own property の符号・確認待ちではない)・CircleErrorsの鍵が ja / en とも 6 つちょうど・ja の文が期待の文と完全に同じ・en に日本語が混ざらない・class の message = ja.json の文・(f) の文が次の押下の動きを約束しない。tests/lib/smartAccount/circleSend.adoptAttempt.test.ts(executor の単体・mappingSaved: false): scan の枝は今までどおり (結果・復旧・(a)/(e)・(b))。予約の直前で止まる (記録なし・90 秒より前の confirmed・revert した confirmed・飛ばす submitting・古い pre-submit) → (f) の error・自分の key の記録なし・署名 0・送信 0・記録が増えない。止まった後に receipt が引けるようになると、次の押下が元の支払いの結果を返す。失効は payment_cancelled が先。省略・true・undefined → 今までどおり。tests/hooks/useBatchPayment.circleStaleSubmitting.test.tsx(本物の executor・本物の storage):tests/hooks/useBatchPayment.circleUnmount.test.tsx(本物の executor・本物の storage):CircleAttemptMappingUnavailableError(符号mapping_unavailable・message は ja の文)・flag は false・hook は error を替えない・記録 0・署名 0・送信 0・removeItem 0 回・warn 1 行 (アドレスなし)。tests/_helpers/circleErrorCases.tsに (f) を足したので、3 フォーム (PaymentForm・CheckoutForm・TipForm) の「符号 → その場面の文 (ja)・英語の画面では英語の文」の表の test が (f) も見る。tests/components/PaymentForm.test.tsxに「Circle の経路・(f) の error → 赤い枠に CircleErrors の文 (ja / en)・「記録を保存できません」の欄は出ない・支払うボタンは押せる (押すと mutate を呼ぶ)」を足した。Circle の経路で既存の欄 (localStorage の flag) を見る test も足した (これまで無かった)。既存の test を変えた所 (main との差)
tests/lib/circlePending.test.tsの「sessionStorage.setItem が throw する環境では毎回新規 UUID (throw せず fallback)」saved: falseを返す」の test 群に置き換えたstableAttemptId(の呼び出しsavedAttemptId) に替えたtests/lib/circleErrorKey.test.tsの「ja と en に 5 つの場面の文があり、余分な鍵が無い」tests/_helpers/circleErrorCases.tstests/hooks/useBatchPayment.circleUnmount.test.tsxの「新しい支払いで、記録の予約を書けない」openScreen・circleStaleSubmittingの bundler の偽物statesを足した・「receipt をまだ返さない」の切り替えを足したtests/lib/smartAccount/circleSend.*.test.ts・tests/hooks/useBatchPayment.circleAdopt.test.tsx・CheckoutForm / TipForm の test は、期待を変えていない。変異 (commit の後に 1 つずつ入れ、関係する test を流し、git checkout で戻した・head ff7e072)
変異の後、作業ツリーは clean (dirty=0)。この head で流し直していない変異 (lib の読み戻しを外す・既にある値でも書く・UUID の採番を try の中へ・warn に sender を入れる・warn を出さない・mappingSaved の省略を false として扱う・付け替えの書き込みを試さない など) は、head 7f3e0c5・e1867490 で落ちることを確かめてあり、その部分のコードは変えていない (warn を try / catch で包んだ以外)。
Q01 について: hook の test は mutate を直接呼ぶので (フォームの「押せない」を通らない)、flag の版でも 2 回目の押下自体は走る。Q01 が落ちるのは「止まった押下の error が (f) であること・flag が立っていないこと」の assert。「flag が立っていなければ・error が (f) なら、ボタンは押せる」は PaymentForm の test が見ている。
検証 (head ff7e072)
tsc --noEmit: exit 0eslint(変えた 11 file・json を除く): exit 0Test Files 926 passed (926)・Tests 25621 passed (25621)・exit 0node scripts/check-bundle-budget.mjs --build(next build を含む・本番 flag): exit 0・全 route が予算内bundle (First Load JS・手元の本番 flag build・gzip の byte)
Route 表は 4 行とも変わらない。予算の数字は変えていない。
残る点 (この PR で直さないもの)
payment.failed・/checkout =checkout.failed・チップ =tip.failed)。この端末の履歴の error の行は、/pay と /checkout だけが書く (チップは履歴の hook を通らないので書かない)。同じ error が同じ秒に重なった分は 1 行にまとまり (履歴の id が同じになる)、localStorage に書けない端末では行は残らない。ボタンが押せるままなので、押した回数に応じて増える。stableAttemptIdの warn だけ。logger は支払いの経路のあちこちで、包まずに呼んでいる (例circle.broadcast.response-lost)。直すなら logger 自体を投げない形にする (別件)。確かめられなかったこと
writeRecord・canStoreRelayIntent) に合わせた。user に見てほしい点
lib/smartAccount/circleSend.tsのexecuteCirclePayment・scan の loop の後・reserveOrResumeの前) と、止めた後の扱い (押せなくしない・符号つきの error・hook は対応を消さない)。circle.attempt_mapping.unavailable) が Sentry に warning として出ること (止まらなかった押下でも出る)。🤖 Generated with Claude Code
https://claude.ai/code/session_01HeizmagJBgL5peL5mQpxkc