From 302fb840866f4ff8fc260b366cfcadd32f4830da Mon Sep 17 00:00:00 2001 From: foXaCe Date: Sun, 18 Jan 2026 11:27:09 +0100 Subject: [PATCH] feat: Support multiple crypto wallets with same token (#676) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Support multiple crypto wallets with same token Allows users to import multiple wallets containing the same cryptocurrency (e.g., ETH on different wallet addresses). Changes: - Add wallet_address column to coinstats_accounts - Update uniqueness validation to include wallet_address - Extract and store wallet address in WalletLinker - Add composite unique index on [item_id, account_id, wallet_address] - Add tests for multi-wallet support and backwards compatibility Users can now have: - ETH (0xAAA...) → "Ethereum (0xAA...AA)" - ETH (0xBBB...) → "Ethereum (0xBB...BB)" Backwards compatible: existing accounts with wallet_address: nil continue to work. * style: Fix array bracket spacing in migration * chore: Update schema.rb with wallet_address column and index Add the missing wallet_address column and composite unique index to db/schema.rb for CI compatibility with db:schema:load * test: Add test for wallet deletion with same token different addresses Verifies that deleting one wallet does not affect other wallets that share the same token but have different addresses. Addresses review comment from @EthanC via @jjmata --------- Signed-off-by: Juan José Mata Co-authored-by: Juan José Mata --- .gitignore | 11 +++ app/models/coinstats_account.rb | 2 +- app/models/coinstats_item/wallet_linker.rb | 3 +- ...dd_wallet_address_to_coinstats_accounts.rb | 16 ++++ db/schema.rb | 5 +- test/models/coinstats_account_test.rb | 91 +++++++++++++++++++ 6 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20260116090336_add_wallet_address_to_coinstats_accounts.rb diff --git a/.gitignore b/.gitignore index ff1f3f80d..d2baf4c85 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,14 @@ scripts/ .cursor/rules/dev_workflow.mdc .cursor/rules/taskmaster.mdc + +# Auto Claude data directory +.auto-claude/ + +# Auto Claude generated files +.auto-claude-security.json +.auto-claude-status +.claude_settings.json +.worktrees/ +.security-key +logs/security/ diff --git a/app/models/coinstats_account.rb b/app/models/coinstats_account.rb index dcab2e186..71c54b5a3 100644 --- a/app/models/coinstats_account.rb +++ b/app/models/coinstats_account.rb @@ -10,7 +10,7 @@ class CoinstatsAccount < ApplicationRecord has_one :account, through: :account_provider, source: :account validates :name, :currency, presence: true - validates :account_id, uniqueness: { scope: :coinstats_item_id, allow_nil: true } + validates :account_id, uniqueness: { scope: [ :coinstats_item_id, :wallet_address ], allow_nil: true } # Alias for compatibility with provider adapter pattern alias_method :current_account, :account diff --git a/app/models/coinstats_item/wallet_linker.rb b/app/models/coinstats_item/wallet_linker.rb index a06a19a59..2840fa35b 100644 --- a/app/models/coinstats_item/wallet_linker.rb +++ b/app/models/coinstats_item/wallet_linker.rb @@ -85,7 +85,8 @@ class CoinstatsItem::WalletLinker name: account_name, currency: "USD", current_balance: current_balance, - account_id: token_id + account_id: token_id, + wallet_address: address ) # Store wallet metadata for future syncs diff --git a/db/migrate/20260116090336_add_wallet_address_to_coinstats_accounts.rb b/db/migrate/20260116090336_add_wallet_address_to_coinstats_accounts.rb new file mode 100644 index 000000000..167aaa339 --- /dev/null +++ b/db/migrate/20260116090336_add_wallet_address_to_coinstats_accounts.rb @@ -0,0 +1,16 @@ +class AddWalletAddressToCoinstatsAccounts < ActiveRecord::Migration[7.2] + def change + add_column :coinstats_accounts, :wallet_address, :string + + # Supprimer l'ancien index simple sur account_id + remove_index :coinstats_accounts, + name: "index_coinstats_accounts_on_account_id", + if_exists: true + + # Créer le nouvel index composite unique + add_index :coinstats_accounts, + [ :coinstats_item_id, :account_id, :wallet_address ], + unique: true, + name: "index_coinstats_accounts_on_item_account_and_wallet" + end +end diff --git a/db/schema.rb b/db/schema.rb index 4e123a2ab..d7a150a26 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_01_15_100001) do +ActiveRecord::Schema[7.2].define(version: 2026_01_16_090336) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -213,7 +213,8 @@ ActiveRecord::Schema[7.2].define(version: 2026_01_15_100001) do t.jsonb "raw_transactions_payload" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["account_id"], name: "index_coinstats_accounts_on_account_id" + t.string "wallet_address" + t.index [ :coinstats_item_id, :account_id, :wallet_address ], name: "index_coinstats_accounts_on_item_account_and_wallet", unique: true t.index ["coinstats_item_id"], name: "index_coinstats_accounts_on_coinstats_item_id" end diff --git a/test/models/coinstats_account_test.rb b/test/models/coinstats_account_test.rb index 773a1bf4f..a4a024d3c 100644 --- a/test/models/coinstats_account_test.rb +++ b/test/models/coinstats_account_test.rb @@ -199,4 +199,95 @@ class CoinstatsAccountTest < ActiveSupport::TestCase assert_equal [], @coinstats_account.raw_transactions_payload end + + # Multi-wallet tests + test "account_id is unique per coinstats_item and wallet_address" do + @coinstats_account.update!( + account_id: "ethereum", + wallet_address: "0xAAA123" + ) + + duplicate = @coinstats_item.coinstats_accounts.build( + name: "Duplicate Ethereum", + currency: "USD", + account_id: "ethereum", + wallet_address: "0xAAA123" + ) + + assert_not duplicate.valid? + assert_includes duplicate.errors[:account_id], "has already been taken" + end + + test "allows same account_id with different wallet_address" do + @coinstats_account.update!( + account_id: "ethereum", + wallet_address: "0xAAA123" + ) + + different_wallet = @coinstats_item.coinstats_accounts.build( + name: "Different Ethereum Wallet", + currency: "USD", + account_id: "ethereum", + wallet_address: "0xBBB456" + ) + + assert different_wallet.valid? + end + + test "allows multiple accounts with nil wallet_address for backwards compatibility" do + first_account = @coinstats_item.coinstats_accounts.create!( + name: "Legacy Account 1", + currency: "USD", + account_id: "bitcoin", + wallet_address: nil + ) + + second_account = @coinstats_item.coinstats_accounts.build( + name: "Legacy Account 2", + currency: "USD", + account_id: "ethereum", + wallet_address: nil + ) + + assert second_account.valid? + assert second_account.save + end + + test "deleting one wallet does not affect other wallets with same token but different address" do + # Create two wallets with the same token (ethereum) but different addresses + wallet_a = @coinstats_item.coinstats_accounts.create!( + name: "Ethereum Wallet A", + currency: "USD", + account_id: "ethereum", + wallet_address: "0xAAA111", + current_balance: 1000.00 + ) + + wallet_b = @coinstats_item.coinstats_accounts.create!( + name: "Ethereum Wallet B", + currency: "USD", + account_id: "ethereum", + wallet_address: "0xBBB222", + current_balance: 2000.00 + ) + + # Verify both wallets exist + assert_equal 3, @coinstats_item.coinstats_accounts.count # includes @coinstats_account from setup + + # Delete wallet A + wallet_a.destroy! + + # Verify only wallet A was deleted, wallet B still exists + @coinstats_item.reload + assert_equal 2, @coinstats_item.coinstats_accounts.count + + # Verify wallet B is still intact with correct data + wallet_b.reload + assert_equal "Ethereum Wallet B", wallet_b.name + assert_equal "0xBBB222", wallet_b.wallet_address + assert_equal BigDecimal("2000.00"), wallet_b.current_balance + + # Verify wallet A no longer exists + assert_nil CoinstatsAccount.find_by(id: wallet_a.id) + end end