mirror of
https://github.com/we-promise/sure.git
synced 2026-09-02 05:11:05 +00:00
fix(onchain-wallets): detect a token-only Solana wallet, and pin asset_kind
Two findings from the review of the previous commits. Detection read only the wallet's lamports, so a wallet emptied of SOL but still holding SPL tokens answered "nothing here". Each token account carries its own rent, so an empty wallet address is not an empty wallet. The token accounts are asked only once the balance comes back zero, so the ordinary case still costs the one request this probe is meant to be, and accounts left behind empty do not count as activity. The narrow blast radius is worth stating: has_activity? only runs when an address matches more than one chain, and when no candidate answers the user is asked to choose rather than turned away. So this was a worse screen, not a rejected wallet. Separately, the check constraint accepted any asset_kind that carried a contract address. Each partial unique index names its kind, so a row with any other one is keyed by nothing and duplicates freely. Adding a token kind already means adding its index here, so pinning the three in the table adds no coupling that the indexes did not already have. Both regression tests fail on the previous code. The third test - emptied token accounts are not activity - passes either way by design: it guards the new branch rather than testing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
db1ff07926
commit
b3dae571bd
@@ -60,7 +60,14 @@ class Onchain::SolanaAdapter
|
||||
def has_activity?(address)
|
||||
return false unless valid_address?(address)
|
||||
|
||||
detection_provider.get_balance(address).positive?
|
||||
return true if detection_provider.get_balance(address).positive?
|
||||
|
||||
# A wallet can hold SPL tokens with no SOL of its own: each token account
|
||||
# carries its own rent, so an empty wallet address is not an empty wallet.
|
||||
# Only asked once the balance comes back zero, so the ordinary case still
|
||||
# costs the single request this probe is meant to be. Emptied token accounts
|
||||
# are left behind on Solana by design and are not activity.
|
||||
held_token_accounts(detection_provider.get_token_accounts(address)).any?
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn("Onchain::SolanaAdapter - activity probe failed: #{e.class}")
|
||||
false
|
||||
|
||||
@@ -63,6 +63,13 @@ class CreateOnchainWalletItemsAndAccounts < ActiveRecord::Migration[7.2]
|
||||
# The model enforces this as well; a direct write does not go through it.
|
||||
t.check_constraint "asset_kind = 'native' OR contract_address IS NOT NULL",
|
||||
name: "chk_onchain_wallet_accounts_token_has_contract"
|
||||
|
||||
# The partial unique indexes below name their kind, so a row carrying any
|
||||
# other one is keyed by nothing and duplicates freely. Adding a token kind
|
||||
# already means adding its index here; this keeps the table from silently
|
||||
# accepting rows for one that has none.
|
||||
t.check_constraint "asset_kind IN ('native', 'erc20', 'spl')",
|
||||
name: "chk_onchain_wallet_accounts_known_asset_kind"
|
||||
end
|
||||
|
||||
add_index :onchain_wallet_accounts, [ :onchain_wallet_item_id, :chain, :wallet_address ],
|
||||
|
||||
Generated
+1
@@ -1526,6 +1526,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_08_20_120000) do
|
||||
t.index ["onchain_wallet_item_id", "chain", "wallet_address"], name: "index_onchain_wallet_accounts_unique_native", unique: true, where: "((asset_kind)::text = 'native'::text)"
|
||||
t.index ["onchain_wallet_item_id"], name: "index_onchain_wallet_accounts_on_onchain_wallet_item_id"
|
||||
t.check_constraint "asset_kind::text = 'native'::text OR contract_address IS NOT NULL", name: "chk_onchain_wallet_accounts_token_has_contract"
|
||||
t.check_constraint "asset_kind::text = ANY (ARRAY['native'::character varying, 'erc20'::character varying, 'spl'::character varying]::text[])", name: "chk_onchain_wallet_accounts_known_asset_kind"
|
||||
end
|
||||
|
||||
create_table "onchain_wallet_items", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
|
||||
@@ -317,6 +317,26 @@ class Onchain::SolanaAdapterTest < ActiveSupport::TestCase
|
||||
assert snapshot.history_truncated?
|
||||
end
|
||||
|
||||
test "a wallet holding SPL tokens but no SOL is still detected" do
|
||||
stub_snapshot(
|
||||
lamports: 0,
|
||||
token_accounts: [ token_account(mint: USDC_MINT, amount: "1000000", decimals: 6) ]
|
||||
)
|
||||
|
||||
# Each token account carries its own rent, so a wallet address emptied of SOL
|
||||
# is not an emptied wallet.
|
||||
assert @adapter.has_activity?(ADDRESS)
|
||||
end
|
||||
|
||||
test "token accounts left behind empty are not activity" do
|
||||
stub_snapshot(
|
||||
lamports: 0,
|
||||
token_accounts: [ token_account(mint: USDC_MINT, amount: "0", decimals: 6) ]
|
||||
)
|
||||
|
||||
assert_not @adapter.has_activity?(ADDRESS)
|
||||
end
|
||||
|
||||
test "detection asks once and does not retry a rate-limited node" do
|
||||
probe = stub_request(:post, Provider::SolanaRpc.url).to_return(status: 429, body: "rate limited")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user