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