fix(backend): move default wallet creation out of middleware - #755
fix(backend): move default wallet creation out of middleware#755DarianM wants to merge 3 commits into
Conversation
WAL-1145 Wallets get created twice: once with default and then with name
When a wallet is created with default if the user leaves the app the wallet is created again when the user rejoins. Expecte: User uses the same wallet Actual: A new default wallet is created If there is already a wallet attached to the user do not create a new wallet. Use the following queries to reproduce: SELECT wallet_id
FROM user_wallets t
WHERE t.user_id IN (
SELECT user_id
FROM user_wallets
GROUP BY user_id
HAVING COUNT(*) > 1
);
select * from wallets where id in (SELECT wallet_id
FROM user_wallets t
WHERE t.user_id IN (
SELECT user_id
FROM user_wallets
GROUP BY user_id
HAVING COUNT(*) > 1
)
)Update: 10.07.2026
Updates 14.07.2026 Root causeThe default wallet was created lazily (as a side effect) inside the HTTP and gRPC middleware, on every authenticated request ("if the user has no wallet, create one"). |
E2E Test ReportE2E markdown report is available as an artifact:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (4)
go/backend/grpc/user.go:14
- Leaving a production TODO that claims the handler is unused is not actionable and can easily become stale/misleading. If
CreateUserDefaultWalletis truly obsolete after moving wallet creation intoCompleteSignup, consider removing the handler (and any service registration / proto exposure) or replacing the TODO with a deprecation comment that includes an issue/ticket and removal timeline.
//TODO: unused handler, nothing calls it.
func (s *rpcService) CreateUserDefaultWallet(ctx context.Context, req *pb.CreateUserDefaultWalletRequest) (*pb.Empty, error) {
go/backend/grpc/signup.go:93
- The error message "signup already completed" is ambiguous for legitimate retries by the same user vs. a conflicting user binding. Consider making the message explicit about the conflict (e.g., that the signup is already associated with a different user) to reduce support/debug time.
if su.UserID != "" && su.UserID != req.UserId {
return nil, ForbiddenError("signup already completed")
}
documentation/docs/signup-guide.md:588
Signup().Completeitself is a fallible step and is executed after the wallet is created, so it’s not accurate to say everything that can fail happens before finalization. Suggest rewording to something like: "all steps that should abort the request (signup ownership check + wallet creation) run beforeSignup().Complete; after completion, only best-effort work remains". This keeps the guide aligned with the actual control flow and failure modes.
**Timing:** Inside `CompleteSignup`, before the signup is marked complete. The handler reads the signup, rejects it if another user already owns it, creates the wallet, and only then calls `Signup().Complete` — so everything that can fail the request happens before the signup is finalized (and before its "new signup" notification fires).
go/backend/grpc/signup_test.go:180
- This test validates the new
CompleteSignupbehavior but does not enforce call order. Since the PR’s correctness relies on doingGet+ walletCreatebeforeSignupService.Complete, consider usinggomock.InOrder(...)(as inTestCompleteSignup_NoAgreementSigning) here (and in similar tests below) to prevent regressions that would still satisfy unordered expectations.
c.SignupService.EXPECT().Complete(gomock.Any(), sID, userID).Return(nil).Times(1)
c.SignupService.EXPECT().Get(gomock.Any(), sID).Return(&signup.Signup{CountryCode: "US"}, nil).Times(1)
c.walletImpl.EXPECT().Create(gomock.Any(), wallets.CreateArgs{UserID: userID, Country: country.US}).Return(nil, nil).Times(1)
Context
Closes WAL-1145
Some users have two wallets, both named
default(
... GROUP BY user_id HAVING COUNT(*) > 1returns duplicates).Root cause
The default wallet was created lazily inside the HTTP and gRPC middleware, on every authenticated request.
On a fresh user's first burst of parallel requests, more than one could pass the "no wallet yet" check and each insert a wallet.
Changes proposed in this pull request
CompleteSignup(
grpc/signup.go), right after Kratos registrationCompleteSignupruns).ops.Createfix: pg advisory lock to prevent double wallet create race condition #326 as the concurrency guard: it serializesconcurrent/retried
CompleteSignupcalls per user (first creates, rest reuse), so a userdoesn't end up with two.