Files
sure/app/models/account_provider.rb
T
buzzromainandClaude Opus 5 2c0d262cf6 fix(onchain): a deleted account leaves no wallet row behind (#3182)
* fix(onchain): a deleted account leaves no wallet row behind

Deleting a Sure account left its on-chain tracking row in place. The row
had a callback to follow its account link into the grave, but the guard
that stopped it destroying itself could not tell apart the two ways that
link dies — by the row, or by the account — and so caught both.

What survived was worse than untidy. The row synced nothing and showed
nowhere, yet it still answered "yes" to "is this address already
tracked?" and still held the asset's slot in the partial unique index.
The address became unusable: adding it again was refused as a duplicate
of something the user could not see, and moving another address onto it
raised a database error.

So three places learn the difference between a row that tracks an
account and one that merely exists: the guard now skips only its own
row's destruction, the family's linked check asks for a live link, and
the linker and address change let a dead row make way instead of
colliding with it.

No migration: a row already orphaned on a running instance is absorbed
the next time that address is tracked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nN1GZi7Dv3d7yZUozw3Yo

* fix(onchain): stop an orphan row passing itself off as a tracked asset

Review on #3182. Both findings are the same root: a row left behind by a
deleted account has no Account and no AccountProvider, but nothing
downstream was asking.

`revise` counted any matching row as tracked, orphans included, so ticking
an asset whose account had been deleted did nothing at all — no Account
created, the row still orphaned, and the screen still reporting the asset
as tracked. It now looks only at rows that are actually linked, and at the
ones that survived the removal pass.

`link` cleared only the rows matching the assets picked in that submit, so
relinking a subset left the rest as orphans. They still show up in
`grouped_accounts` and token review as tracked, and — before the fix above
— they also stopped `revise` from ever rebuilding them. Reclaiming an
address now clears every row it left behind. A live row is untouched, with
a test that fails if the sweep widens.

One existing test had to change its premise rather than its assertion. It
built two unlinked rows and asserted the surviving row object was the same
one; unlinked rows are orphans, so revising now rebuilds them and the
survivor is a new row. Linking them first is what a tracked asset actually
is, and the assertion then measures the removal it was written for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 08:22:17 +02:00

60 lines
2.2 KiB
Ruby

class AccountProvider < ApplicationRecord
belongs_to :account
belongs_to :provider, polymorphic: true
has_many :holdings, dependent: :nullify
validates :account_id, uniqueness: { scope: :provider_type }
validates :provider_id, uniqueness: { scope: :provider_type }
# When unlinking a CoinStats account, also destroy the CoinstatsAccount record
# so it doesn't remain orphaned and count as "needs setup".
# Other providers may legitimately enter a "needs setup" state.
after_destroy :destroy_coinstats_provider_account, if: :coinstats_provider?
# An on-chain tracking row IS the link: unlike a bank connection there is
# nothing to reconnect to and nothing worth keeping. Left behind by a generic
# unlink it stops syncing, because the syncer only reads linked rows, while
# its partial unique index still holds the (item, chain, address, asset) slot
# — so linking that same asset again would collide with a row nothing shows.
# The Sure account and its holdings are untouched and carry on as manual.
after_destroy :destroy_onchain_provider_account, if: :onchain_provider?
# Returns the provider adapter for this connection
def adapter
Provider::Factory.create_adapter(provider, account: account)
end
# Convenience method to get provider name
# Delegates to the adapter for consistency, falls back to underscored provider_type
def provider_name
adapter&.provider_name || provider_type.underscore
end
private
def coinstats_provider?
provider_type == "CoinstatsAccount"
end
def destroy_coinstats_provider_account
provider&.destroy
end
def onchain_provider?
provider_type == "OnchainWalletAccount"
end
def destroy_onchain_provider_account
# Skipped only when the row is the one destroying this link, through its
# own dependent: :destroy — answering that by destroying the row again
# would go round in circles. The account destroys this link by association
# too, and there the row must follow: a guard reading nothing but
# `destroyed_by_association` could not tell the two apart, and left the
# row behind with no account to track.
return if destroyed_by_association&.active_record == OnchainWalletAccount
provider&.destroy
end
end