From 6ca4b5fc06e9e25f63c58b9e1da1b150fe1b754e Mon Sep 17 00:00:00 2001 From: Gustavo Stingelin Date: Fri, 10 Jul 2026 15:39:54 -0700 Subject: [PATCH] wallet/sql: preserve legacy wallet schema In this commit, we add the wallet, scope, account, and address schema shared by the SQLite and PostgreSQL backends. The schema keeps the current dual-passphrase encryption hierarchy, watch-only nullability, encrypted public material, scope account counters, and SHA256 address identifiers. It also persists the sticky used bit directly, so the SQL port does not change gap-limit behavior or require the key-vault rewrite. Extracted-from: bbfe24b9d3fff620cd77c4806e58b416af401322 Extracted-from: b6b6a7e895c785e020d61d28bb5fe882e8947cc5 Extracted-from: 9a313ea6ffaa0f9c04385a2a9c5436acef83be5f Extracted-from: bb5a67bcbcdd1fbc694c4e5e6eb9b921970149e1 Extracted-from: c5d82b214fb8696bd4619bf3e0362bb5f35a0585 Extracted-from: 5a639d4dfb9350d0c8e93699fd105a83e0351c7f Extracted-from: 99e40ab81c0939faa4b2de1774130cfcb0c1bc7c Extracted-from: 132ba5b512677f5ea71f9031e078959399918549 Extracted-from: 47b750bc30e10e7da235acc57a31874a7f2fc954 Extracted-from: 69e459bc66c5daf00f19eedcf500a694a65d27e1 Co-authored-by: yyforyongyu --- wallet/internal/sql/pg/legacy_schema_test.go | 216 +++++++++++++ .../sql/pg/migrations/000002_wallets.down.sql | 2 + .../sql/pg/migrations/000002_wallets.up.sql | 38 +++ .../migrations/000003_address_types.down.sql | 1 + .../pg/migrations/000003_address_types.up.sql | 16 + .../pg/migrations/000004_key_scopes.down.sql | 1 + .../pg/migrations/000004_key_scopes.up.sql | 21 ++ .../pg/migrations/000005_accounts.down.sql | 1 + .../sql/pg/migrations/000005_accounts.up.sql | 47 +++ .../pg/migrations/000006_addresses.down.sql | 3 + .../sql/pg/migrations/000006_addresses.up.sql | 80 +++++ wallet/internal/sql/pg/migrations_test.go | 29 +- wallet/internal/sql/pg/queries/accounts.sql | 60 ++++ .../internal/sql/pg/queries/address_types.sql | 4 + wallet/internal/sql/pg/queries/addresses.sql | 70 +++++ wallet/internal/sql/pg/queries/key_scopes.sql | 37 +++ wallet/internal/sql/pg/queries/wallets.sql | 84 +++++ wallet/internal/sql/pg/sqlc/accounts.sql.go | 205 ++++++++++++ .../internal/sql/pg/sqlc/address_types.sql.go | 39 +++ wallet/internal/sql/pg/sqlc/addresses.sql.go | 216 +++++++++++++ wallet/internal/sql/pg/sqlc/db.go | 224 ++++++++++++- wallet/internal/sql/pg/sqlc/key_scopes.sql.go | 135 ++++++++ wallet/internal/sql/pg/sqlc/models.go | 79 +++++ wallet/internal/sql/pg/sqlc/querier.go | 20 ++ wallet/internal/sql/pg/sqlc/wallets.sql.go | 257 +++++++++++++++ .../internal/sql/sqlite/legacy_schema_test.go | 297 ++++++++++++++++++ .../sqlite/migrations/000002_wallets.down.sql | 2 + .../sqlite/migrations/000002_wallets.up.sql | 39 +++ .../migrations/000003_address_types.down.sql | 1 + .../migrations/000003_address_types.up.sql | 16 + .../migrations/000004_key_scopes.down.sql | 1 + .../migrations/000004_key_scopes.up.sql | 21 ++ .../migrations/000005_accounts.down.sql | 1 + .../sqlite/migrations/000005_accounts.up.sql | 47 +++ .../migrations/000006_addresses.down.sql | 2 + .../sqlite/migrations/000006_addresses.up.sql | 73 +++++ wallet/internal/sql/sqlite/migrations_test.go | 32 +- .../internal/sql/sqlite/queries/accounts.sql | 60 ++++ .../sql/sqlite/queries/address_types.sql | 4 + .../internal/sql/sqlite/queries/addresses.sql | 67 ++++ .../sql/sqlite/queries/key_scopes.sql | 37 +++ .../internal/sql/sqlite/queries/wallets.sql | 84 +++++ .../internal/sql/sqlite/sqlc/accounts.sql.go | 205 ++++++++++++ .../sql/sqlite/sqlc/address_types.sql.go | 39 +++ .../internal/sql/sqlite/sqlc/addresses.sql.go | 213 +++++++++++++ wallet/internal/sql/sqlite/sqlc/db.go | 224 ++++++++++++- .../sql/sqlite/sqlc/key_scopes.sql.go | 135 ++++++++ wallet/internal/sql/sqlite/sqlc/models.go | 79 +++++ wallet/internal/sql/sqlite/sqlc/querier.go | 20 ++ .../internal/sql/sqlite/sqlc/wallets.sql.go | 257 +++++++++++++++ 50 files changed, 3793 insertions(+), 48 deletions(-) create mode 100644 wallet/internal/sql/pg/legacy_schema_test.go create mode 100644 wallet/internal/sql/pg/migrations/000002_wallets.down.sql create mode 100644 wallet/internal/sql/pg/migrations/000002_wallets.up.sql create mode 100644 wallet/internal/sql/pg/migrations/000003_address_types.down.sql create mode 100644 wallet/internal/sql/pg/migrations/000003_address_types.up.sql create mode 100644 wallet/internal/sql/pg/migrations/000004_key_scopes.down.sql create mode 100644 wallet/internal/sql/pg/migrations/000004_key_scopes.up.sql create mode 100644 wallet/internal/sql/pg/migrations/000005_accounts.down.sql create mode 100644 wallet/internal/sql/pg/migrations/000005_accounts.up.sql create mode 100644 wallet/internal/sql/pg/migrations/000006_addresses.down.sql create mode 100644 wallet/internal/sql/pg/migrations/000006_addresses.up.sql create mode 100644 wallet/internal/sql/pg/queries/accounts.sql create mode 100644 wallet/internal/sql/pg/queries/address_types.sql create mode 100644 wallet/internal/sql/pg/queries/addresses.sql create mode 100644 wallet/internal/sql/pg/queries/key_scopes.sql create mode 100644 wallet/internal/sql/pg/queries/wallets.sql create mode 100644 wallet/internal/sql/pg/sqlc/accounts.sql.go create mode 100644 wallet/internal/sql/pg/sqlc/address_types.sql.go create mode 100644 wallet/internal/sql/pg/sqlc/addresses.sql.go create mode 100644 wallet/internal/sql/pg/sqlc/key_scopes.sql.go create mode 100644 wallet/internal/sql/pg/sqlc/wallets.sql.go create mode 100644 wallet/internal/sql/sqlite/legacy_schema_test.go create mode 100644 wallet/internal/sql/sqlite/migrations/000002_wallets.down.sql create mode 100644 wallet/internal/sql/sqlite/migrations/000002_wallets.up.sql create mode 100644 wallet/internal/sql/sqlite/migrations/000003_address_types.down.sql create mode 100644 wallet/internal/sql/sqlite/migrations/000003_address_types.up.sql create mode 100644 wallet/internal/sql/sqlite/migrations/000004_key_scopes.down.sql create mode 100644 wallet/internal/sql/sqlite/migrations/000004_key_scopes.up.sql create mode 100644 wallet/internal/sql/sqlite/migrations/000005_accounts.down.sql create mode 100644 wallet/internal/sql/sqlite/migrations/000005_accounts.up.sql create mode 100644 wallet/internal/sql/sqlite/migrations/000006_addresses.down.sql create mode 100644 wallet/internal/sql/sqlite/migrations/000006_addresses.up.sql create mode 100644 wallet/internal/sql/sqlite/queries/accounts.sql create mode 100644 wallet/internal/sql/sqlite/queries/address_types.sql create mode 100644 wallet/internal/sql/sqlite/queries/addresses.sql create mode 100644 wallet/internal/sql/sqlite/queries/key_scopes.sql create mode 100644 wallet/internal/sql/sqlite/queries/wallets.sql create mode 100644 wallet/internal/sql/sqlite/sqlc/accounts.sql.go create mode 100644 wallet/internal/sql/sqlite/sqlc/address_types.sql.go create mode 100644 wallet/internal/sql/sqlite/sqlc/addresses.sql.go create mode 100644 wallet/internal/sql/sqlite/sqlc/key_scopes.sql.go create mode 100644 wallet/internal/sql/sqlite/sqlc/wallets.sql.go diff --git a/wallet/internal/sql/pg/legacy_schema_test.go b/wallet/internal/sql/pg/legacy_schema_test.go new file mode 100644 index 0000000000..aa9bcc891d --- /dev/null +++ b/wallet/internal/sql/pg/legacy_schema_test.go @@ -0,0 +1,216 @@ +//go:build test_db_postgres + +package pg + +import ( + "context" + "crypto/sha256" + "database/sql" + "testing" + "time" + + "github.com/btcsuite/btcwallet/wallet/internal/sql/pg/sqlc" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" +) + +// TestLegacySchemaQueries verifies the legacy ciphertext and sticky address +// state through the generated PostgreSQL queries. +func TestLegacySchemaQueries(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + container, err := postgres.Run( + ctx, "postgres:18-alpine", + postgres.WithDatabase("btcwallet"), + postgres.WithUsername("postgres"), + postgres.WithPassword("postgres"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2).WithStartupTimeout(2*time.Minute), + ), + ) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, container.Terminate(context.Background())) + }) + + dsn, err := container.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + db, err := Open(ctx, Config{DSN: dsn}) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + require.NoError(t, ApplyMigrations(db)) + requireNoPostgresPlaintextColumns(t, db) + + queries := sqlc.New(db) + genesisHash := pgBytesOf(0x01, 32) + birthdayHash := pgBytesOf(0x02, 32) + require.NoError(t, queries.InsertBlock(ctx, sqlc.InsertBlockParams{ + BlockHeight: 0, + HeaderHash: genesisHash, + BlockTimestamp: 1, + })) + require.NoError(t, queries.InsertBlock(ctx, sqlc.InsertBlockParams{ + BlockHeight: 100, + HeaderHash: birthdayHash, + BlockTimestamp: 2, + })) + + walletID, err := queries.CreateWallet(ctx, sqlc.CreateWalletParams{ + WalletName: "watch-only", + ManagerVersion: 8, + ManagerCreatedAt: 123, + IsWatchOnly: true, + MasterPubParams: []byte("encrypted-master-public-params"), + EncryptedCryptoPubKey: []byte("encrypted-crypto-public-key"), + }) + require.NoError(t, err) + wallet, err := queries.GetWalletByName(ctx, "watch-only") + require.NoError(t, err) + require.Equal(t, int64(123), wallet.ManagerCreatedAt) + require.Nil(t, wallet.MasterPrivParams) + require.Nil(t, wallet.EncryptedCryptoPrivKey) + require.Equal(t, []byte("encrypted-crypto-public-key"), + wallet.EncryptedCryptoPubKey) + + require.NoError(t, queries.PutWalletSyncState( + ctx, sqlc.PutWalletSyncStateParams{ + WalletID: walletID, + StartBlockHeight: 0, + SyncedBlockHeight: 100, + BirthdayTimestamp: 1234, + BirthdayBlockHeight: sql.NullInt32{Int32: 100, Valid: true}, + BirthdayBlockVerified: true, + }, + )) + syncState, err := queries.GetWalletSyncState(ctx, walletID) + require.NoError(t, err) + require.Equal(t, genesisHash, syncState.StartBlockHash) + require.Equal(t, birthdayHash, syncState.BirthdayBlockHash) + + scopeID, err := queries.CreateKeyScope(ctx, sqlc.CreateKeyScopeParams{ + WalletID: walletID, + Purpose: 84, + CoinType: 0, + ExternalAddrType: 4, + InternalAddrType: 4, + }) + require.NoError(t, err) + rows, err := queries.UpdateLastAccountNumber( + ctx, sqlc.UpdateLastAccountNumberParams{ + LastAccountNumber: sql.NullInt64{Int64: 7, Valid: true}, + ID: scopeID, + WalletID: walletID, + }, + ) + require.NoError(t, err) + require.Equal(t, int64(1), rows) + scope, err := queries.GetKeyScope(ctx, sqlc.GetKeyScopeParams{ + WalletID: walletID, + Purpose: 84, + CoinType: 0, + }) + require.NoError(t, err) + require.Equal(t, int64(7), scope.LastAccountNumber.Int64) + err = queries.CreateAccount(ctx, sqlc.CreateAccountParams{ + WalletID: walletID, + ScopeID: scopeID, + AccountNumber: 7, + AccountType: 1, + AccountName: "imported", + EncryptedPubKey: []byte("encrypted-account-public-key"), + MasterKeyFingerprint: sql.NullInt64{Int64: 42, Valid: true}, + ExternalAddrType: sql.NullInt16{Int16: 3, Valid: true}, + InternalAddrType: sql.NullInt16{Int16: 4, Valid: true}, + }) + require.NoError(t, err) + + watchOnlyScriptHash := sha256.Sum256([]byte("watch-only-script-address-id")) + require.NoError(t, queries.CreateAddress(ctx, sqlc.CreateAddressParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: watchOnlyScriptHash[:], + AccountNumber: 7, + AddressType: 2, + AddedAt: 12, + SyncStatus: 2, + EncryptedHash: []byte("encrypted-watch-only-script-hash"), + })) + + addressHash := sha256.Sum256([]byte("imported-address-id")) + require.NoError(t, queries.CreateAddress(ctx, sqlc.CreateAddressParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: addressHash[:], + AccountNumber: 7, + AddressType: 1, + AddedAt: 11, + SyncStatus: 2, + EncryptedPubKey: []byte("encrypted-imported-public-key"), + EncryptedPrivKey: []byte{}, + })) + rows, err = queries.MarkAddressUsed(ctx, sqlc.MarkAddressUsedParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: addressHash[:], + }) + require.NoError(t, err) + require.Equal(t, int64(1), rows) + address, err := queries.GetAddress(ctx, sqlc.GetAddressParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: addressHash[:], + }) + require.NoError(t, err) + require.True(t, address.Used) + require.Equal(t, []byte("encrypted-imported-public-key"), + address.EncryptedPubKey) + + _, err = db.ExecContext(ctx, ` + UPDATE addresses SET used = FALSE + WHERE wallet_id = $1 AND scope_id = $2 AND address_hash = $3 + `, walletID, scopeID, addressHash[:]) + require.ErrorContains(t, err, "address used state cannot be cleared") +} + +func requireNoPostgresPlaintextColumns(t *testing.T, db *sql.DB) { + t.Helper() + + columns := map[string][]string{ + "wallets": {"master_hd_pub_key"}, + "key_scopes": {"coin_pub_key", "coin_priv_key"}, + "accounts": {"public_key", "private_key"}, + "addresses": {"address_id", "script_pub_key", "pub_key", "script"}, + } + for table, tableColumns := range columns { + for _, column := range tableColumns { + var exists bool + err := db.QueryRow(` + SELECT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = $1 + AND column_name = $2 + ) + `, table, column).Scan(&exists) + require.NoError(t, err) + require.False(t, exists, "%s.%s", table, column) + } + } +} + +func pgBytesOf(value byte, count int) []byte { + b := make([]byte, count) + for i := range b { + b[i] = value + } + + return b +} diff --git a/wallet/internal/sql/pg/migrations/000002_wallets.down.sql b/wallet/internal/sql/pg/migrations/000002_wallets.down.sql new file mode 100644 index 0000000000..0b73d6e376 --- /dev/null +++ b/wallet/internal/sql/pg/migrations/000002_wallets.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS wallet_sync_states; +DROP TABLE IF EXISTS wallets; diff --git a/wallet/internal/sql/pg/migrations/000002_wallets.up.sql b/wallet/internal/sql/pg/migrations/000002_wallets.up.sql new file mode 100644 index 0000000000..096952f593 --- /dev/null +++ b/wallet/internal/sql/pg/migrations/000002_wallets.up.sql @@ -0,0 +1,38 @@ +-- The wallets table preserves the legacy waddrmgr encryption hierarchy. +CREATE TABLE wallets ( + id BIGSERIAL PRIMARY KEY, + wallet_name TEXT NOT NULL UNIQUE, + manager_version BIGINT NOT NULL + CHECK (manager_version BETWEEN 0 AND 4294967295), + manager_created_at BIGINT NOT NULL CHECK (manager_created_at >= 0), + is_watch_only BOOLEAN NOT NULL, + master_pub_params BYTEA NOT NULL, + master_priv_params BYTEA, + encrypted_crypto_pub_key BYTEA NOT NULL, + encrypted_crypto_priv_key BYTEA, + encrypted_crypto_script_key BYTEA, + encrypted_master_hd_pub_key BYTEA, + encrypted_master_hd_priv_key BYTEA +); + +-- The wallet sync state mirrors the block stamps and birthday metadata stored +-- by the legacy address manager. +CREATE TABLE wallet_sync_states ( + wallet_id BIGINT PRIMARY KEY, + start_block_height INTEGER NOT NULL, + synced_block_height INTEGER NOT NULL, + birthday_timestamp BIGINT NOT NULL CHECK (birthday_timestamp >= 0), + birthday_block_height INTEGER, + birthday_block_verified BOOLEAN NOT NULL DEFAULT FALSE, + FOREIGN KEY (wallet_id) REFERENCES wallets (id) ON DELETE RESTRICT, + FOREIGN KEY (start_block_height) REFERENCES blocks (block_height) + ON DELETE RESTRICT, + FOREIGN KEY (synced_block_height) REFERENCES blocks (block_height) + ON DELETE RESTRICT, + FOREIGN KEY (birthday_block_height) REFERENCES blocks (block_height) + ON DELETE RESTRICT, + CHECK ( + birthday_block_verified = FALSE + OR birthday_block_height IS NOT NULL + ) +); diff --git a/wallet/internal/sql/pg/migrations/000003_address_types.down.sql b/wallet/internal/sql/pg/migrations/000003_address_types.down.sql new file mode 100644 index 0000000000..a8937217d1 --- /dev/null +++ b/wallet/internal/sql/pg/migrations/000003_address_types.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS address_types; diff --git a/wallet/internal/sql/pg/migrations/000003_address_types.up.sql b/wallet/internal/sql/pg/migrations/000003_address_types.up.sql new file mode 100644 index 0000000000..dcfba1e88b --- /dev/null +++ b/wallet/internal/sql/pg/migrations/000003_address_types.up.sql @@ -0,0 +1,16 @@ +-- Address types are the stable public waddrmgr.AddressType values used by key +-- scope address schemas. +CREATE TABLE address_types ( + id SMALLINT PRIMARY KEY CHECK (id BETWEEN 0 AND 7), + type_name TEXT NOT NULL UNIQUE +); + +INSERT INTO address_types (id, type_name) VALUES + (0, 'pubkey-hash'), + (1, 'script'), + (2, 'raw-pubkey'), + (3, 'nested-witness-pubkey'), + (4, 'witness-pubkey'), + (5, 'witness-script'), + (6, 'taproot-pubkey'), + (7, 'taproot-script'); diff --git a/wallet/internal/sql/pg/migrations/000004_key_scopes.down.sql b/wallet/internal/sql/pg/migrations/000004_key_scopes.down.sql new file mode 100644 index 0000000000..ae9bf8f8bc --- /dev/null +++ b/wallet/internal/sql/pg/migrations/000004_key_scopes.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS key_scopes; diff --git a/wallet/internal/sql/pg/migrations/000004_key_scopes.up.sql b/wallet/internal/sql/pg/migrations/000004_key_scopes.up.sql new file mode 100644 index 0000000000..c4d5d494a3 --- /dev/null +++ b/wallet/internal/sql/pg/migrations/000004_key_scopes.up.sql @@ -0,0 +1,21 @@ +-- Key scopes preserve encrypted coin keys and the address schema associated +-- with each purpose and coin tuple. +CREATE TABLE key_scopes ( + id BIGSERIAL PRIMARY KEY, + wallet_id BIGINT NOT NULL, + purpose BIGINT NOT NULL CHECK (purpose BETWEEN 0 AND 4294967295), + coin_type BIGINT NOT NULL CHECK (coin_type BETWEEN 0 AND 4294967295), + encrypted_coin_pub_key BYTEA, + encrypted_coin_priv_key BYTEA, + last_account_number BIGINT + CHECK (last_account_number BETWEEN 0 AND 4294967295), + external_addr_type SMALLINT NOT NULL, + internal_addr_type SMALLINT NOT NULL, + FOREIGN KEY (wallet_id) REFERENCES wallets (id) ON DELETE RESTRICT, + FOREIGN KEY (external_addr_type) REFERENCES address_types (id) + ON DELETE RESTRICT, + FOREIGN KEY (internal_addr_type) REFERENCES address_types (id) + ON DELETE RESTRICT, + UNIQUE (wallet_id, purpose, coin_type), + UNIQUE (id, wallet_id) +); diff --git a/wallet/internal/sql/pg/migrations/000005_accounts.down.sql b/wallet/internal/sql/pg/migrations/000005_accounts.down.sql new file mode 100644 index 0000000000..1616db4979 --- /dev/null +++ b/wallet/internal/sql/pg/migrations/000005_accounts.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS accounts; diff --git a/wallet/internal/sql/pg/migrations/000005_accounts.up.sql b/wallet/internal/sql/pg/migrations/000005_accounts.up.sql new file mode 100644 index 0000000000..28069f0a7c --- /dev/null +++ b/wallet/internal/sql/pg/migrations/000005_accounts.up.sql @@ -0,0 +1,47 @@ +-- Accounts preserve the two stable legacy account encodings. Every account, +-- including an imported watch-only account, retains its uint32 account number. +CREATE TABLE accounts ( + wallet_id BIGINT NOT NULL, + scope_id BIGINT NOT NULL, + account_number BIGINT NOT NULL + CHECK (account_number BETWEEN 0 AND 4294967295), + account_type SMALLINT NOT NULL CHECK (account_type IN (0, 1)), + account_name TEXT NOT NULL, + encrypted_pub_key BYTEA NOT NULL, + encrypted_priv_key BYTEA, + master_key_fingerprint BIGINT + CHECK ( + master_key_fingerprint IS NULL + OR master_key_fingerprint BETWEEN 0 AND 4294967295 + ), + next_external_index BIGINT NOT NULL DEFAULT 0 + CHECK (next_external_index BETWEEN 0 AND 4294967295), + next_internal_index BIGINT NOT NULL DEFAULT 0 + CHECK (next_internal_index BETWEEN 0 AND 4294967295), + external_addr_type SMALLINT, + internal_addr_type SMALLINT, + FOREIGN KEY (scope_id, wallet_id) REFERENCES key_scopes (id, wallet_id) + ON DELETE RESTRICT, + FOREIGN KEY (external_addr_type) REFERENCES address_types (id) + ON DELETE RESTRICT, + FOREIGN KEY (internal_addr_type) REFERENCES address_types (id) + ON DELETE RESTRICT, + PRIMARY KEY (scope_id, account_number), + UNIQUE (scope_id, account_name), + UNIQUE (scope_id, account_number, wallet_id), + CHECK ( + (external_addr_type IS NULL AND internal_addr_type IS NULL) + OR (external_addr_type IS NOT NULL AND internal_addr_type IS NOT NULL) + ), + CHECK ( + (account_type = 0 + AND master_key_fingerprint IS NULL + AND external_addr_type IS NULL + AND internal_addr_type IS NULL) + OR ( + account_type = 1 + AND encrypted_priv_key IS NULL + AND master_key_fingerprint IS NOT NULL + ) + ) +); diff --git a/wallet/internal/sql/pg/migrations/000006_addresses.down.sql b/wallet/internal/sql/pg/migrations/000006_addresses.down.sql new file mode 100644 index 0000000000..266c5a6fcc --- /dev/null +++ b/wallet/internal/sql/pg/migrations/000006_addresses.down.sql @@ -0,0 +1,3 @@ +DROP TRIGGER IF EXISTS trg_addresses_used_is_monotonic ON addresses; +DROP FUNCTION IF EXISTS assert_address_used_is_monotonic(); +DROP TABLE IF EXISTS addresses; diff --git a/wallet/internal/sql/pg/migrations/000006_addresses.up.sql b/wallet/internal/sql/pg/migrations/000006_addresses.up.sql new file mode 100644 index 0000000000..445aaef7e9 --- /dev/null +++ b/wallet/internal/sql/pg/migrations/000006_addresses.up.sql @@ -0,0 +1,80 @@ +-- Addresses are keyed by SHA256(addressID), matching the legacy address and +-- used-address buckets without exposing the encoded address in plaintext. +CREATE TABLE addresses ( + wallet_id BIGINT NOT NULL, + scope_id BIGINT NOT NULL, + address_hash BYTEA NOT NULL CHECK (length(address_hash) = 32), + account_number BIGINT NOT NULL + CHECK (account_number BETWEEN 0 AND 4294967295), + address_type SMALLINT NOT NULL CHECK (address_type BETWEEN 0 AND 4), + added_at BIGINT NOT NULL CHECK (added_at >= 0), + sync_status SMALLINT NOT NULL CHECK (sync_status IN (0, 1, 2)), + branch BIGINT CHECK (branch BETWEEN 0 AND 4294967295), + address_index BIGINT CHECK (address_index BETWEEN 0 AND 4294967295), + encrypted_pub_key BYTEA, + encrypted_priv_key BYTEA, + encrypted_hash BYTEA, + encrypted_script BYTEA, + witness_version SMALLINT CHECK (witness_version BETWEEN 0 AND 255), + is_secret_script BOOLEAN, + used BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (wallet_id, scope_id, address_hash), + FOREIGN KEY (scope_id, account_number, wallet_id) + REFERENCES accounts (scope_id, account_number, wallet_id) + ON DELETE RESTRICT, + CHECK ( + (address_type = 0 + AND branch IS NOT NULL + AND address_index IS NOT NULL + AND encrypted_pub_key IS NULL + AND encrypted_priv_key IS NULL + AND encrypted_hash IS NULL + AND encrypted_script IS NULL + AND witness_version IS NULL + AND is_secret_script IS NULL) + OR (address_type = 1 + AND branch IS NULL + AND address_index IS NULL + AND encrypted_pub_key IS NOT NULL + AND encrypted_hash IS NULL + AND encrypted_script IS NULL + AND witness_version IS NULL + AND is_secret_script IS NULL) + OR (address_type = 2 + AND branch IS NULL + AND address_index IS NULL + AND encrypted_pub_key IS NULL + AND encrypted_priv_key IS NULL + AND encrypted_hash IS NOT NULL + AND witness_version IS NULL + AND is_secret_script IS NULL) + OR (address_type IN (3, 4) + AND branch IS NULL + AND address_index IS NULL + AND encrypted_pub_key IS NULL + AND encrypted_priv_key IS NULL + AND encrypted_hash IS NOT NULL + AND witness_version IS NOT NULL + AND is_secret_script IS NOT NULL) + ) +); + +CREATE INDEX idx_addresses_account +ON addresses (scope_id, account_number, address_hash); + +-- The used bit is sticky, as it is in the legacy used-address bucket. +CREATE FUNCTION assert_address_used_is_monotonic() RETURNS TRIGGER AS $$ +BEGIN + IF OLD.used AND NOT NEW.used THEN + RAISE EXCEPTION 'address used state cannot be cleared' + USING ERRCODE = '23514'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_addresses_used_is_monotonic +BEFORE UPDATE OF used ON addresses +FOR EACH ROW +EXECUTE FUNCTION assert_address_used_is_monotonic(); diff --git a/wallet/internal/sql/pg/migrations_test.go b/wallet/internal/sql/pg/migrations_test.go index 8ee5ca796a..018d1c27ee 100644 --- a/wallet/internal/sql/pg/migrations_test.go +++ b/wallet/internal/sql/pg/migrations_test.go @@ -49,25 +49,32 @@ func TestMigrationsRoundTrip(t *testing.T) { require.NoError(t, ApplyMigrations(db)) require.Zero(t, db.Stats().InUse) require.NoError(t, db.PingContext(ctx)) - requireBlocksTable(t, db, true) + requireSchemaTables(t, db, true) require.NoError(t, RollbackMigrations(db)) require.Zero(t, db.Stats().InUse) require.NoError(t, db.PingContext(ctx)) - requireBlocksTable(t, db, false) + requireSchemaTables(t, db, false) require.NoError(t, ApplyMigrations(db)) - requireBlocksTable(t, db, true) + requireSchemaTables(t, db, true) } -func requireBlocksTable(t *testing.T, db *sql.DB, expected bool) { +func requireSchemaTables(t *testing.T, db *sql.DB, expected bool) { t.Helper() - var exists bool - err := db.QueryRow( - "SELECT EXISTS (SELECT 1 FROM information_schema.tables " + - "WHERE table_schema = 'public' AND table_name = 'blocks')", - ).Scan(&exists) - require.NoError(t, err) - require.Equal(t, expected, exists) + tables := []string{ + "blocks", "wallets", "wallet_sync_states", "address_types", + "key_scopes", "accounts", "addresses", + } + for _, table := range tables { + var exists bool + err := db.QueryRow( + "SELECT EXISTS (SELECT 1 FROM information_schema.tables "+ + "WHERE table_schema = 'public' AND table_name = $1)", + table, + ).Scan(&exists) + require.NoError(t, err) + require.Equal(t, expected, exists, table) + } } diff --git a/wallet/internal/sql/pg/queries/accounts.sql b/wallet/internal/sql/pg/queries/accounts.sql new file mode 100644 index 0000000000..e3d779ad16 --- /dev/null +++ b/wallet/internal/sql/pg/queries/accounts.sql @@ -0,0 +1,60 @@ +-- name: CreateAccount :exec +INSERT INTO accounts ( + wallet_id, + scope_id, + account_number, + account_type, + account_name, + encrypted_pub_key, + encrypted_priv_key, + master_key_fingerprint, + next_external_index, + next_internal_index, + external_addr_type, + internal_addr_type +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12); + +-- name: GetAccount :one +SELECT + wallet_id, + scope_id, + account_number, + account_type, + account_name, + encrypted_pub_key, + encrypted_priv_key, + master_key_fingerprint, + next_external_index, + next_internal_index, + external_addr_type, + internal_addr_type +FROM accounts +WHERE scope_id = $1 AND account_number = $2; + +-- name: ListAccounts :many +SELECT + wallet_id, + scope_id, + account_number, + account_type, + account_name, + encrypted_pub_key, + encrypted_priv_key, + master_key_fingerprint, + next_external_index, + next_internal_index, + external_addr_type, + internal_addr_type +FROM accounts +WHERE scope_id = $1 +ORDER BY account_number; + +-- name: RenameAccount :execrows +UPDATE accounts +SET account_name = $1 +WHERE scope_id = $2 AND account_number = $3; + +-- name: UpdateAccountIndexes :execrows +UPDATE accounts +SET next_external_index = $1, next_internal_index = $2 +WHERE scope_id = $3 AND account_number = $4; diff --git a/wallet/internal/sql/pg/queries/address_types.sql b/wallet/internal/sql/pg/queries/address_types.sql new file mode 100644 index 0000000000..879c9df0e2 --- /dev/null +++ b/wallet/internal/sql/pg/queries/address_types.sql @@ -0,0 +1,4 @@ +-- name: ListAddressTypes :many +SELECT id, type_name +FROM address_types +ORDER BY id; diff --git a/wallet/internal/sql/pg/queries/addresses.sql b/wallet/internal/sql/pg/queries/addresses.sql new file mode 100644 index 0000000000..dfc342d631 --- /dev/null +++ b/wallet/internal/sql/pg/queries/addresses.sql @@ -0,0 +1,70 @@ +-- name: CreateAddress :exec +INSERT INTO addresses ( + wallet_id, + scope_id, + address_hash, + account_number, + address_type, + added_at, + sync_status, + branch, + address_index, + encrypted_pub_key, + encrypted_priv_key, + encrypted_hash, + encrypted_script, + witness_version, + is_secret_script, + used +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, + $9, $10, $11, $12, $13, $14, $15, $16 +); + +-- name: GetAddress :one +SELECT + wallet_id, + scope_id, + address_hash, + account_number, + address_type, + added_at, + sync_status, + branch, + address_index, + encrypted_pub_key, + encrypted_priv_key, + encrypted_hash, + encrypted_script, + witness_version, + is_secret_script, + used +FROM addresses +WHERE wallet_id = $1 AND scope_id = $2 AND address_hash = $3; + +-- name: ListAccountAddresses :many +SELECT + wallet_id, + scope_id, + address_hash, + account_number, + address_type, + added_at, + sync_status, + branch, + address_index, + encrypted_pub_key, + encrypted_priv_key, + encrypted_hash, + encrypted_script, + witness_version, + is_secret_script, + used +FROM addresses +WHERE scope_id = $1 AND account_number = $2 +ORDER BY address_hash; + +-- name: MarkAddressUsed :execrows +UPDATE addresses +SET used = TRUE +WHERE wallet_id = $1 AND scope_id = $2 AND address_hash = $3; diff --git a/wallet/internal/sql/pg/queries/key_scopes.sql b/wallet/internal/sql/pg/queries/key_scopes.sql new file mode 100644 index 0000000000..020b9c664f --- /dev/null +++ b/wallet/internal/sql/pg/queries/key_scopes.sql @@ -0,0 +1,37 @@ +-- name: CreateKeyScope :one +INSERT INTO key_scopes ( + wallet_id, + purpose, + coin_type, + encrypted_coin_pub_key, + encrypted_coin_priv_key, + external_addr_type, + internal_addr_type +) VALUES ($1, $2, $3, $4, $5, $6, $7) +RETURNING id; + +-- name: GetKeyScope :one +SELECT + id, + wallet_id, + purpose, + coin_type, + encrypted_coin_pub_key, + encrypted_coin_priv_key, + last_account_number, + external_addr_type, + internal_addr_type +FROM key_scopes +WHERE wallet_id = $1 AND purpose = $2 AND coin_type = $3; + +-- name: UpdateKeyScopeKeys :execrows +UPDATE key_scopes +SET + encrypted_coin_pub_key = $1, + encrypted_coin_priv_key = $2 +WHERE id = $3 AND wallet_id = $4; + +-- name: UpdateLastAccountNumber :execrows +UPDATE key_scopes +SET last_account_number = $1 +WHERE id = $2 AND wallet_id = $3; diff --git a/wallet/internal/sql/pg/queries/wallets.sql b/wallet/internal/sql/pg/queries/wallets.sql new file mode 100644 index 0000000000..f3a487ef39 --- /dev/null +++ b/wallet/internal/sql/pg/queries/wallets.sql @@ -0,0 +1,84 @@ +-- name: CreateWallet :one +INSERT INTO wallets ( + wallet_name, + manager_version, + manager_created_at, + is_watch_only, + master_pub_params, + master_priv_params, + encrypted_crypto_pub_key, + encrypted_crypto_priv_key, + encrypted_crypto_script_key, + encrypted_master_hd_pub_key, + encrypted_master_hd_priv_key +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) +RETURNING id; + +-- name: GetWalletByName :one +SELECT + id, + wallet_name, + manager_version, + manager_created_at, + is_watch_only, + master_pub_params, + master_priv_params, + encrypted_crypto_pub_key, + encrypted_crypto_priv_key, + encrypted_crypto_script_key, + encrypted_master_hd_pub_key, + encrypted_master_hd_priv_key +FROM wallets +WHERE wallet_name = $1; + +-- name: UpdateWalletEncryption :execrows +UPDATE wallets +SET + master_pub_params = $1, + master_priv_params = $2, + encrypted_crypto_pub_key = $3, + encrypted_crypto_priv_key = $4, + encrypted_crypto_script_key = $5, + encrypted_master_hd_pub_key = $6, + encrypted_master_hd_priv_key = $7 +WHERE id = $8; + +-- name: PutWalletSyncState :exec +INSERT INTO wallet_sync_states ( + wallet_id, + start_block_height, + synced_block_height, + birthday_timestamp, + birthday_block_height, + birthday_block_verified +) VALUES ($1, $2, $3, $4, $5, $6); + +-- name: GetWalletSyncState :one +SELECT + s.wallet_id, + s.start_block_height, + start_block.header_hash AS start_block_hash, + s.synced_block_height, + synced_block.header_hash AS synced_block_hash, + s.birthday_timestamp, + s.birthday_block_height, + birthday_block.header_hash AS birthday_block_hash, + s.birthday_block_verified +FROM wallet_sync_states AS s +INNER JOIN blocks AS start_block + ON s.start_block_height = start_block.block_height +INNER JOIN blocks AS synced_block + ON s.synced_block_height = synced_block.block_height +LEFT JOIN blocks AS birthday_block + ON s.birthday_block_height = birthday_block.block_height +WHERE s.wallet_id = $1; + +-- name: UpdateWalletSyncState :execrows +UPDATE wallet_sync_states +SET + start_block_height = $1, + synced_block_height = $2, + birthday_timestamp = $3, + birthday_block_height = $4, + birthday_block_verified = $5 +WHERE wallet_id = $6; diff --git a/wallet/internal/sql/pg/sqlc/accounts.sql.go b/wallet/internal/sql/pg/sqlc/accounts.sql.go new file mode 100644 index 0000000000..8e3387762f --- /dev/null +++ b/wallet/internal/sql/pg/sqlc/accounts.sql.go @@ -0,0 +1,205 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: accounts.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const CreateAccount = `-- name: CreateAccount :exec +INSERT INTO accounts ( + wallet_id, + scope_id, + account_number, + account_type, + account_name, + encrypted_pub_key, + encrypted_priv_key, + master_key_fingerprint, + next_external_index, + next_internal_index, + external_addr_type, + internal_addr_type +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) +` + +type CreateAccountParams struct { + WalletID int64 + ScopeID int64 + AccountNumber int64 + AccountType int16 + AccountName string + EncryptedPubKey []byte + EncryptedPrivKey []byte + MasterKeyFingerprint sql.NullInt64 + NextExternalIndex int64 + NextInternalIndex int64 + ExternalAddrType sql.NullInt16 + InternalAddrType sql.NullInt16 +} + +func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) error { + _, err := q.exec(ctx, q.createAccountStmt, CreateAccount, + arg.WalletID, + arg.ScopeID, + arg.AccountNumber, + arg.AccountType, + arg.AccountName, + arg.EncryptedPubKey, + arg.EncryptedPrivKey, + arg.MasterKeyFingerprint, + arg.NextExternalIndex, + arg.NextInternalIndex, + arg.ExternalAddrType, + arg.InternalAddrType, + ) + return err +} + +const GetAccount = `-- name: GetAccount :one +SELECT + wallet_id, + scope_id, + account_number, + account_type, + account_name, + encrypted_pub_key, + encrypted_priv_key, + master_key_fingerprint, + next_external_index, + next_internal_index, + external_addr_type, + internal_addr_type +FROM accounts +WHERE scope_id = $1 AND account_number = $2 +` + +type GetAccountParams struct { + ScopeID int64 + AccountNumber int64 +} + +func (q *Queries) GetAccount(ctx context.Context, arg GetAccountParams) (Account, error) { + row := q.queryRow(ctx, q.getAccountStmt, GetAccount, arg.ScopeID, arg.AccountNumber) + var i Account + err := row.Scan( + &i.WalletID, + &i.ScopeID, + &i.AccountNumber, + &i.AccountType, + &i.AccountName, + &i.EncryptedPubKey, + &i.EncryptedPrivKey, + &i.MasterKeyFingerprint, + &i.NextExternalIndex, + &i.NextInternalIndex, + &i.ExternalAddrType, + &i.InternalAddrType, + ) + return i, err +} + +const ListAccounts = `-- name: ListAccounts :many +SELECT + wallet_id, + scope_id, + account_number, + account_type, + account_name, + encrypted_pub_key, + encrypted_priv_key, + master_key_fingerprint, + next_external_index, + next_internal_index, + external_addr_type, + internal_addr_type +FROM accounts +WHERE scope_id = $1 +ORDER BY account_number +` + +func (q *Queries) ListAccounts(ctx context.Context, scopeID int64) ([]Account, error) { + rows, err := q.query(ctx, q.listAccountsStmt, ListAccounts, scopeID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Account + for rows.Next() { + var i Account + if err := rows.Scan( + &i.WalletID, + &i.ScopeID, + &i.AccountNumber, + &i.AccountType, + &i.AccountName, + &i.EncryptedPubKey, + &i.EncryptedPrivKey, + &i.MasterKeyFingerprint, + &i.NextExternalIndex, + &i.NextInternalIndex, + &i.ExternalAddrType, + &i.InternalAddrType, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const RenameAccount = `-- name: RenameAccount :execrows +UPDATE accounts +SET account_name = $1 +WHERE scope_id = $2 AND account_number = $3 +` + +type RenameAccountParams struct { + AccountName string + ScopeID int64 + AccountNumber int64 +} + +func (q *Queries) RenameAccount(ctx context.Context, arg RenameAccountParams) (int64, error) { + result, err := q.exec(ctx, q.renameAccountStmt, RenameAccount, arg.AccountName, arg.ScopeID, arg.AccountNumber) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const UpdateAccountIndexes = `-- name: UpdateAccountIndexes :execrows +UPDATE accounts +SET next_external_index = $1, next_internal_index = $2 +WHERE scope_id = $3 AND account_number = $4 +` + +type UpdateAccountIndexesParams struct { + NextExternalIndex int64 + NextInternalIndex int64 + ScopeID int64 + AccountNumber int64 +} + +func (q *Queries) UpdateAccountIndexes(ctx context.Context, arg UpdateAccountIndexesParams) (int64, error) { + result, err := q.exec(ctx, q.updateAccountIndexesStmt, UpdateAccountIndexes, + arg.NextExternalIndex, + arg.NextInternalIndex, + arg.ScopeID, + arg.AccountNumber, + ) + if err != nil { + return 0, err + } + return result.RowsAffected() +} diff --git a/wallet/internal/sql/pg/sqlc/address_types.sql.go b/wallet/internal/sql/pg/sqlc/address_types.sql.go new file mode 100644 index 0000000000..54b21a9a9f --- /dev/null +++ b/wallet/internal/sql/pg/sqlc/address_types.sql.go @@ -0,0 +1,39 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: address_types.sql + +package sqlc + +import ( + "context" +) + +const ListAddressTypes = `-- name: ListAddressTypes :many +SELECT id, type_name +FROM address_types +ORDER BY id +` + +func (q *Queries) ListAddressTypes(ctx context.Context) ([]AddressType, error) { + rows, err := q.query(ctx, q.listAddressTypesStmt, ListAddressTypes) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AddressType + for rows.Next() { + var i AddressType + if err := rows.Scan(&i.ID, &i.TypeName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/wallet/internal/sql/pg/sqlc/addresses.sql.go b/wallet/internal/sql/pg/sqlc/addresses.sql.go new file mode 100644 index 0000000000..3e0b27d49f --- /dev/null +++ b/wallet/internal/sql/pg/sqlc/addresses.sql.go @@ -0,0 +1,216 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: addresses.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const CreateAddress = `-- name: CreateAddress :exec +INSERT INTO addresses ( + wallet_id, + scope_id, + address_hash, + account_number, + address_type, + added_at, + sync_status, + branch, + address_index, + encrypted_pub_key, + encrypted_priv_key, + encrypted_hash, + encrypted_script, + witness_version, + is_secret_script, + used +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, + $9, $10, $11, $12, $13, $14, $15, $16 +) +` + +type CreateAddressParams struct { + WalletID int64 + ScopeID int64 + AddressHash []byte + AccountNumber int64 + AddressType int16 + AddedAt int64 + SyncStatus int16 + Branch sql.NullInt64 + AddressIndex sql.NullInt64 + EncryptedPubKey []byte + EncryptedPrivKey []byte + EncryptedHash []byte + EncryptedScript []byte + WitnessVersion sql.NullInt16 + IsSecretScript sql.NullBool + Used bool +} + +func (q *Queries) CreateAddress(ctx context.Context, arg CreateAddressParams) error { + _, err := q.exec(ctx, q.createAddressStmt, CreateAddress, + arg.WalletID, + arg.ScopeID, + arg.AddressHash, + arg.AccountNumber, + arg.AddressType, + arg.AddedAt, + arg.SyncStatus, + arg.Branch, + arg.AddressIndex, + arg.EncryptedPubKey, + arg.EncryptedPrivKey, + arg.EncryptedHash, + arg.EncryptedScript, + arg.WitnessVersion, + arg.IsSecretScript, + arg.Used, + ) + return err +} + +const GetAddress = `-- name: GetAddress :one +SELECT + wallet_id, + scope_id, + address_hash, + account_number, + address_type, + added_at, + sync_status, + branch, + address_index, + encrypted_pub_key, + encrypted_priv_key, + encrypted_hash, + encrypted_script, + witness_version, + is_secret_script, + used +FROM addresses +WHERE wallet_id = $1 AND scope_id = $2 AND address_hash = $3 +` + +type GetAddressParams struct { + WalletID int64 + ScopeID int64 + AddressHash []byte +} + +func (q *Queries) GetAddress(ctx context.Context, arg GetAddressParams) (Address, error) { + row := q.queryRow(ctx, q.getAddressStmt, GetAddress, arg.WalletID, arg.ScopeID, arg.AddressHash) + var i Address + err := row.Scan( + &i.WalletID, + &i.ScopeID, + &i.AddressHash, + &i.AccountNumber, + &i.AddressType, + &i.AddedAt, + &i.SyncStatus, + &i.Branch, + &i.AddressIndex, + &i.EncryptedPubKey, + &i.EncryptedPrivKey, + &i.EncryptedHash, + &i.EncryptedScript, + &i.WitnessVersion, + &i.IsSecretScript, + &i.Used, + ) + return i, err +} + +const ListAccountAddresses = `-- name: ListAccountAddresses :many +SELECT + wallet_id, + scope_id, + address_hash, + account_number, + address_type, + added_at, + sync_status, + branch, + address_index, + encrypted_pub_key, + encrypted_priv_key, + encrypted_hash, + encrypted_script, + witness_version, + is_secret_script, + used +FROM addresses +WHERE scope_id = $1 AND account_number = $2 +ORDER BY address_hash +` + +type ListAccountAddressesParams struct { + ScopeID int64 + AccountNumber int64 +} + +func (q *Queries) ListAccountAddresses(ctx context.Context, arg ListAccountAddressesParams) ([]Address, error) { + rows, err := q.query(ctx, q.listAccountAddressesStmt, ListAccountAddresses, arg.ScopeID, arg.AccountNumber) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Address + for rows.Next() { + var i Address + if err := rows.Scan( + &i.WalletID, + &i.ScopeID, + &i.AddressHash, + &i.AccountNumber, + &i.AddressType, + &i.AddedAt, + &i.SyncStatus, + &i.Branch, + &i.AddressIndex, + &i.EncryptedPubKey, + &i.EncryptedPrivKey, + &i.EncryptedHash, + &i.EncryptedScript, + &i.WitnessVersion, + &i.IsSecretScript, + &i.Used, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const MarkAddressUsed = `-- name: MarkAddressUsed :execrows +UPDATE addresses +SET used = TRUE +WHERE wallet_id = $1 AND scope_id = $2 AND address_hash = $3 +` + +type MarkAddressUsedParams struct { + WalletID int64 + ScopeID int64 + AddressHash []byte +} + +func (q *Queries) MarkAddressUsed(ctx context.Context, arg MarkAddressUsedParams) (int64, error) { + result, err := q.exec(ctx, q.markAddressUsedStmt, MarkAddressUsed, arg.WalletID, arg.ScopeID, arg.AddressHash) + if err != nil { + return 0, err + } + return result.RowsAffected() +} diff --git a/wallet/internal/sql/pg/sqlc/db.go b/wallet/internal/sql/pg/sqlc/db.go index 168d8395d4..3357da35b6 100644 --- a/wallet/internal/sql/pg/sqlc/db.go +++ b/wallet/internal/sql/pg/sqlc/db.go @@ -24,28 +24,118 @@ func New(db DBTX) *Queries { func Prepare(ctx context.Context, db DBTX) (*Queries, error) { q := Queries{db: db} var err error + if q.createAccountStmt, err = db.PrepareContext(ctx, CreateAccount); err != nil { + return nil, fmt.Errorf("error preparing query CreateAccount: %w", err) + } + if q.createAddressStmt, err = db.PrepareContext(ctx, CreateAddress); err != nil { + return nil, fmt.Errorf("error preparing query CreateAddress: %w", err) + } + if q.createKeyScopeStmt, err = db.PrepareContext(ctx, CreateKeyScope); err != nil { + return nil, fmt.Errorf("error preparing query CreateKeyScope: %w", err) + } + if q.createWalletStmt, err = db.PrepareContext(ctx, CreateWallet); err != nil { + return nil, fmt.Errorf("error preparing query CreateWallet: %w", err) + } if q.deleteBlockStmt, err = db.PrepareContext(ctx, DeleteBlock); err != nil { return nil, fmt.Errorf("error preparing query DeleteBlock: %w", err) } + if q.getAccountStmt, err = db.PrepareContext(ctx, GetAccount); err != nil { + return nil, fmt.Errorf("error preparing query GetAccount: %w", err) + } + if q.getAddressStmt, err = db.PrepareContext(ctx, GetAddress); err != nil { + return nil, fmt.Errorf("error preparing query GetAddress: %w", err) + } if q.getBlockByHeightStmt, err = db.PrepareContext(ctx, GetBlockByHeight); err != nil { return nil, fmt.Errorf("error preparing query GetBlockByHeight: %w", err) } if q.getBlocksInRangeStmt, err = db.PrepareContext(ctx, GetBlocksInRange); err != nil { return nil, fmt.Errorf("error preparing query GetBlocksInRange: %w", err) } + if q.getKeyScopeStmt, err = db.PrepareContext(ctx, GetKeyScope); err != nil { + return nil, fmt.Errorf("error preparing query GetKeyScope: %w", err) + } + if q.getWalletByNameStmt, err = db.PrepareContext(ctx, GetWalletByName); err != nil { + return nil, fmt.Errorf("error preparing query GetWalletByName: %w", err) + } + if q.getWalletSyncStateStmt, err = db.PrepareContext(ctx, GetWalletSyncState); err != nil { + return nil, fmt.Errorf("error preparing query GetWalletSyncState: %w", err) + } if q.insertBlockStmt, err = db.PrepareContext(ctx, InsertBlock); err != nil { return nil, fmt.Errorf("error preparing query InsertBlock: %w", err) } + if q.listAccountAddressesStmt, err = db.PrepareContext(ctx, ListAccountAddresses); err != nil { + return nil, fmt.Errorf("error preparing query ListAccountAddresses: %w", err) + } + if q.listAccountsStmt, err = db.PrepareContext(ctx, ListAccounts); err != nil { + return nil, fmt.Errorf("error preparing query ListAccounts: %w", err) + } + if q.listAddressTypesStmt, err = db.PrepareContext(ctx, ListAddressTypes); err != nil { + return nil, fmt.Errorf("error preparing query ListAddressTypes: %w", err) + } + if q.markAddressUsedStmt, err = db.PrepareContext(ctx, MarkAddressUsed); err != nil { + return nil, fmt.Errorf("error preparing query MarkAddressUsed: %w", err) + } + if q.putWalletSyncStateStmt, err = db.PrepareContext(ctx, PutWalletSyncState); err != nil { + return nil, fmt.Errorf("error preparing query PutWalletSyncState: %w", err) + } + if q.renameAccountStmt, err = db.PrepareContext(ctx, RenameAccount); err != nil { + return nil, fmt.Errorf("error preparing query RenameAccount: %w", err) + } + if q.updateAccountIndexesStmt, err = db.PrepareContext(ctx, UpdateAccountIndexes); err != nil { + return nil, fmt.Errorf("error preparing query UpdateAccountIndexes: %w", err) + } + if q.updateKeyScopeKeysStmt, err = db.PrepareContext(ctx, UpdateKeyScopeKeys); err != nil { + return nil, fmt.Errorf("error preparing query UpdateKeyScopeKeys: %w", err) + } + if q.updateLastAccountNumberStmt, err = db.PrepareContext(ctx, UpdateLastAccountNumber); err != nil { + return nil, fmt.Errorf("error preparing query UpdateLastAccountNumber: %w", err) + } + if q.updateWalletEncryptionStmt, err = db.PrepareContext(ctx, UpdateWalletEncryption); err != nil { + return nil, fmt.Errorf("error preparing query UpdateWalletEncryption: %w", err) + } + if q.updateWalletSyncStateStmt, err = db.PrepareContext(ctx, UpdateWalletSyncState); err != nil { + return nil, fmt.Errorf("error preparing query UpdateWalletSyncState: %w", err) + } return &q, nil } func (q *Queries) Close() error { var err error + if q.createAccountStmt != nil { + if cerr := q.createAccountStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createAccountStmt: %w", cerr) + } + } + if q.createAddressStmt != nil { + if cerr := q.createAddressStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createAddressStmt: %w", cerr) + } + } + if q.createKeyScopeStmt != nil { + if cerr := q.createKeyScopeStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createKeyScopeStmt: %w", cerr) + } + } + if q.createWalletStmt != nil { + if cerr := q.createWalletStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createWalletStmt: %w", cerr) + } + } if q.deleteBlockStmt != nil { if cerr := q.deleteBlockStmt.Close(); cerr != nil { err = fmt.Errorf("error closing deleteBlockStmt: %w", cerr) } } + if q.getAccountStmt != nil { + if cerr := q.getAccountStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getAccountStmt: %w", cerr) + } + } + if q.getAddressStmt != nil { + if cerr := q.getAddressStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getAddressStmt: %w", cerr) + } + } if q.getBlockByHeightStmt != nil { if cerr := q.getBlockByHeightStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getBlockByHeightStmt: %w", cerr) @@ -56,11 +146,81 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getBlocksInRangeStmt: %w", cerr) } } + if q.getKeyScopeStmt != nil { + if cerr := q.getKeyScopeStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getKeyScopeStmt: %w", cerr) + } + } + if q.getWalletByNameStmt != nil { + if cerr := q.getWalletByNameStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getWalletByNameStmt: %w", cerr) + } + } + if q.getWalletSyncStateStmt != nil { + if cerr := q.getWalletSyncStateStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getWalletSyncStateStmt: %w", cerr) + } + } if q.insertBlockStmt != nil { if cerr := q.insertBlockStmt.Close(); cerr != nil { err = fmt.Errorf("error closing insertBlockStmt: %w", cerr) } } + if q.listAccountAddressesStmt != nil { + if cerr := q.listAccountAddressesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listAccountAddressesStmt: %w", cerr) + } + } + if q.listAccountsStmt != nil { + if cerr := q.listAccountsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listAccountsStmt: %w", cerr) + } + } + if q.listAddressTypesStmt != nil { + if cerr := q.listAddressTypesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listAddressTypesStmt: %w", cerr) + } + } + if q.markAddressUsedStmt != nil { + if cerr := q.markAddressUsedStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing markAddressUsedStmt: %w", cerr) + } + } + if q.putWalletSyncStateStmt != nil { + if cerr := q.putWalletSyncStateStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing putWalletSyncStateStmt: %w", cerr) + } + } + if q.renameAccountStmt != nil { + if cerr := q.renameAccountStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing renameAccountStmt: %w", cerr) + } + } + if q.updateAccountIndexesStmt != nil { + if cerr := q.updateAccountIndexesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateAccountIndexesStmt: %w", cerr) + } + } + if q.updateKeyScopeKeysStmt != nil { + if cerr := q.updateKeyScopeKeysStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateKeyScopeKeysStmt: %w", cerr) + } + } + if q.updateLastAccountNumberStmt != nil { + if cerr := q.updateLastAccountNumberStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateLastAccountNumberStmt: %w", cerr) + } + } + if q.updateWalletEncryptionStmt != nil { + if cerr := q.updateWalletEncryptionStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateWalletEncryptionStmt: %w", cerr) + } + } + if q.updateWalletSyncStateStmt != nil { + if cerr := q.updateWalletSyncStateStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateWalletSyncStateStmt: %w", cerr) + } + } return err } @@ -98,21 +258,61 @@ func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, ar } type Queries struct { - db DBTX - tx *sql.Tx - deleteBlockStmt *sql.Stmt - getBlockByHeightStmt *sql.Stmt - getBlocksInRangeStmt *sql.Stmt - insertBlockStmt *sql.Stmt + db DBTX + tx *sql.Tx + createAccountStmt *sql.Stmt + createAddressStmt *sql.Stmt + createKeyScopeStmt *sql.Stmt + createWalletStmt *sql.Stmt + deleteBlockStmt *sql.Stmt + getAccountStmt *sql.Stmt + getAddressStmt *sql.Stmt + getBlockByHeightStmt *sql.Stmt + getBlocksInRangeStmt *sql.Stmt + getKeyScopeStmt *sql.Stmt + getWalletByNameStmt *sql.Stmt + getWalletSyncStateStmt *sql.Stmt + insertBlockStmt *sql.Stmt + listAccountAddressesStmt *sql.Stmt + listAccountsStmt *sql.Stmt + listAddressTypesStmt *sql.Stmt + markAddressUsedStmt *sql.Stmt + putWalletSyncStateStmt *sql.Stmt + renameAccountStmt *sql.Stmt + updateAccountIndexesStmt *sql.Stmt + updateKeyScopeKeysStmt *sql.Stmt + updateLastAccountNumberStmt *sql.Stmt + updateWalletEncryptionStmt *sql.Stmt + updateWalletSyncStateStmt *sql.Stmt } func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{ - db: tx, - tx: tx, - deleteBlockStmt: q.deleteBlockStmt, - getBlockByHeightStmt: q.getBlockByHeightStmt, - getBlocksInRangeStmt: q.getBlocksInRangeStmt, - insertBlockStmt: q.insertBlockStmt, + db: tx, + tx: tx, + createAccountStmt: q.createAccountStmt, + createAddressStmt: q.createAddressStmt, + createKeyScopeStmt: q.createKeyScopeStmt, + createWalletStmt: q.createWalletStmt, + deleteBlockStmt: q.deleteBlockStmt, + getAccountStmt: q.getAccountStmt, + getAddressStmt: q.getAddressStmt, + getBlockByHeightStmt: q.getBlockByHeightStmt, + getBlocksInRangeStmt: q.getBlocksInRangeStmt, + getKeyScopeStmt: q.getKeyScopeStmt, + getWalletByNameStmt: q.getWalletByNameStmt, + getWalletSyncStateStmt: q.getWalletSyncStateStmt, + insertBlockStmt: q.insertBlockStmt, + listAccountAddressesStmt: q.listAccountAddressesStmt, + listAccountsStmt: q.listAccountsStmt, + listAddressTypesStmt: q.listAddressTypesStmt, + markAddressUsedStmt: q.markAddressUsedStmt, + putWalletSyncStateStmt: q.putWalletSyncStateStmt, + renameAccountStmt: q.renameAccountStmt, + updateAccountIndexesStmt: q.updateAccountIndexesStmt, + updateKeyScopeKeysStmt: q.updateKeyScopeKeysStmt, + updateLastAccountNumberStmt: q.updateLastAccountNumberStmt, + updateWalletEncryptionStmt: q.updateWalletEncryptionStmt, + updateWalletSyncStateStmt: q.updateWalletSyncStateStmt, } } diff --git a/wallet/internal/sql/pg/sqlc/key_scopes.sql.go b/wallet/internal/sql/pg/sqlc/key_scopes.sql.go new file mode 100644 index 0000000000..2aec105862 --- /dev/null +++ b/wallet/internal/sql/pg/sqlc/key_scopes.sql.go @@ -0,0 +1,135 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: key_scopes.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const CreateKeyScope = `-- name: CreateKeyScope :one +INSERT INTO key_scopes ( + wallet_id, + purpose, + coin_type, + encrypted_coin_pub_key, + encrypted_coin_priv_key, + external_addr_type, + internal_addr_type +) VALUES ($1, $2, $3, $4, $5, $6, $7) +RETURNING id +` + +type CreateKeyScopeParams struct { + WalletID int64 + Purpose int64 + CoinType int64 + EncryptedCoinPubKey []byte + EncryptedCoinPrivKey []byte + ExternalAddrType int16 + InternalAddrType int16 +} + +func (q *Queries) CreateKeyScope(ctx context.Context, arg CreateKeyScopeParams) (int64, error) { + row := q.queryRow(ctx, q.createKeyScopeStmt, CreateKeyScope, + arg.WalletID, + arg.Purpose, + arg.CoinType, + arg.EncryptedCoinPubKey, + arg.EncryptedCoinPrivKey, + arg.ExternalAddrType, + arg.InternalAddrType, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const GetKeyScope = `-- name: GetKeyScope :one +SELECT + id, + wallet_id, + purpose, + coin_type, + encrypted_coin_pub_key, + encrypted_coin_priv_key, + last_account_number, + external_addr_type, + internal_addr_type +FROM key_scopes +WHERE wallet_id = $1 AND purpose = $2 AND coin_type = $3 +` + +type GetKeyScopeParams struct { + WalletID int64 + Purpose int64 + CoinType int64 +} + +func (q *Queries) GetKeyScope(ctx context.Context, arg GetKeyScopeParams) (KeyScope, error) { + row := q.queryRow(ctx, q.getKeyScopeStmt, GetKeyScope, arg.WalletID, arg.Purpose, arg.CoinType) + var i KeyScope + err := row.Scan( + &i.ID, + &i.WalletID, + &i.Purpose, + &i.CoinType, + &i.EncryptedCoinPubKey, + &i.EncryptedCoinPrivKey, + &i.LastAccountNumber, + &i.ExternalAddrType, + &i.InternalAddrType, + ) + return i, err +} + +const UpdateKeyScopeKeys = `-- name: UpdateKeyScopeKeys :execrows +UPDATE key_scopes +SET + encrypted_coin_pub_key = $1, + encrypted_coin_priv_key = $2 +WHERE id = $3 AND wallet_id = $4 +` + +type UpdateKeyScopeKeysParams struct { + EncryptedCoinPubKey []byte + EncryptedCoinPrivKey []byte + ID int64 + WalletID int64 +} + +func (q *Queries) UpdateKeyScopeKeys(ctx context.Context, arg UpdateKeyScopeKeysParams) (int64, error) { + result, err := q.exec(ctx, q.updateKeyScopeKeysStmt, UpdateKeyScopeKeys, + arg.EncryptedCoinPubKey, + arg.EncryptedCoinPrivKey, + arg.ID, + arg.WalletID, + ) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const UpdateLastAccountNumber = `-- name: UpdateLastAccountNumber :execrows +UPDATE key_scopes +SET last_account_number = $1 +WHERE id = $2 AND wallet_id = $3 +` + +type UpdateLastAccountNumberParams struct { + LastAccountNumber sql.NullInt64 + ID int64 + WalletID int64 +} + +func (q *Queries) UpdateLastAccountNumber(ctx context.Context, arg UpdateLastAccountNumberParams) (int64, error) { + result, err := q.exec(ctx, q.updateLastAccountNumberStmt, UpdateLastAccountNumber, arg.LastAccountNumber, arg.ID, arg.WalletID) + if err != nil { + return 0, err + } + return result.RowsAffected() +} diff --git a/wallet/internal/sql/pg/sqlc/models.go b/wallet/internal/sql/pg/sqlc/models.go index 9b53f59718..a0e553959a 100644 --- a/wallet/internal/sql/pg/sqlc/models.go +++ b/wallet/internal/sql/pg/sqlc/models.go @@ -4,8 +4,87 @@ package sqlc +import ( + "database/sql" +) + +type Account struct { + WalletID int64 + ScopeID int64 + AccountNumber int64 + AccountType int16 + AccountName string + EncryptedPubKey []byte + EncryptedPrivKey []byte + MasterKeyFingerprint sql.NullInt64 + NextExternalIndex int64 + NextInternalIndex int64 + ExternalAddrType sql.NullInt16 + InternalAddrType sql.NullInt16 +} + +type Address struct { + WalletID int64 + ScopeID int64 + AddressHash []byte + AccountNumber int64 + AddressType int16 + AddedAt int64 + SyncStatus int16 + Branch sql.NullInt64 + AddressIndex sql.NullInt64 + EncryptedPubKey []byte + EncryptedPrivKey []byte + EncryptedHash []byte + EncryptedScript []byte + WitnessVersion sql.NullInt16 + IsSecretScript sql.NullBool + Used bool +} + +type AddressType struct { + ID int16 + TypeName string +} + type Block struct { BlockHeight int32 HeaderHash []byte BlockTimestamp int64 } + +type KeyScope struct { + ID int64 + WalletID int64 + Purpose int64 + CoinType int64 + EncryptedCoinPubKey []byte + EncryptedCoinPrivKey []byte + LastAccountNumber sql.NullInt64 + ExternalAddrType int16 + InternalAddrType int16 +} + +type Wallet struct { + ID int64 + WalletName string + ManagerVersion int64 + ManagerCreatedAt int64 + IsWatchOnly bool + MasterPubParams []byte + MasterPrivParams []byte + EncryptedCryptoPubKey []byte + EncryptedCryptoPrivKey []byte + EncryptedCryptoScriptKey []byte + EncryptedMasterHdPubKey []byte + EncryptedMasterHdPrivKey []byte +} + +type WalletSyncState struct { + WalletID int64 + StartBlockHeight int32 + SyncedBlockHeight int32 + BirthdayTimestamp int64 + BirthdayBlockHeight sql.NullInt32 + BirthdayBlockVerified bool +} diff --git a/wallet/internal/sql/pg/sqlc/querier.go b/wallet/internal/sql/pg/sqlc/querier.go index 8ba410fea5..660e1042ad 100644 --- a/wallet/internal/sql/pg/sqlc/querier.go +++ b/wallet/internal/sql/pg/sqlc/querier.go @@ -9,10 +9,30 @@ import ( ) type Querier interface { + CreateAccount(ctx context.Context, arg CreateAccountParams) error + CreateAddress(ctx context.Context, arg CreateAddressParams) error + CreateKeyScope(ctx context.Context, arg CreateKeyScopeParams) (int64, error) + CreateWallet(ctx context.Context, arg CreateWalletParams) (int64, error) DeleteBlock(ctx context.Context, blockHeight int32) error + GetAccount(ctx context.Context, arg GetAccountParams) (Account, error) + GetAddress(ctx context.Context, arg GetAddressParams) (Address, error) GetBlockByHeight(ctx context.Context, blockHeight int32) (Block, error) GetBlocksInRange(ctx context.Context, arg GetBlocksInRangeParams) ([]Block, error) + GetKeyScope(ctx context.Context, arg GetKeyScopeParams) (KeyScope, error) + GetWalletByName(ctx context.Context, walletName string) (Wallet, error) + GetWalletSyncState(ctx context.Context, walletID int64) (GetWalletSyncStateRow, error) InsertBlock(ctx context.Context, arg InsertBlockParams) error + ListAccountAddresses(ctx context.Context, arg ListAccountAddressesParams) ([]Address, error) + ListAccounts(ctx context.Context, scopeID int64) ([]Account, error) + ListAddressTypes(ctx context.Context) ([]AddressType, error) + MarkAddressUsed(ctx context.Context, arg MarkAddressUsedParams) (int64, error) + PutWalletSyncState(ctx context.Context, arg PutWalletSyncStateParams) error + RenameAccount(ctx context.Context, arg RenameAccountParams) (int64, error) + UpdateAccountIndexes(ctx context.Context, arg UpdateAccountIndexesParams) (int64, error) + UpdateKeyScopeKeys(ctx context.Context, arg UpdateKeyScopeKeysParams) (int64, error) + UpdateLastAccountNumber(ctx context.Context, arg UpdateLastAccountNumberParams) (int64, error) + UpdateWalletEncryption(ctx context.Context, arg UpdateWalletEncryptionParams) (int64, error) + UpdateWalletSyncState(ctx context.Context, arg UpdateWalletSyncStateParams) (int64, error) } var _ Querier = (*Queries)(nil) diff --git a/wallet/internal/sql/pg/sqlc/wallets.sql.go b/wallet/internal/sql/pg/sqlc/wallets.sql.go new file mode 100644 index 0000000000..d7cef6f3a2 --- /dev/null +++ b/wallet/internal/sql/pg/sqlc/wallets.sql.go @@ -0,0 +1,257 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: wallets.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const CreateWallet = `-- name: CreateWallet :one +INSERT INTO wallets ( + wallet_name, + manager_version, + manager_created_at, + is_watch_only, + master_pub_params, + master_priv_params, + encrypted_crypto_pub_key, + encrypted_crypto_priv_key, + encrypted_crypto_script_key, + encrypted_master_hd_pub_key, + encrypted_master_hd_priv_key +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) +RETURNING id +` + +type CreateWalletParams struct { + WalletName string + ManagerVersion int64 + ManagerCreatedAt int64 + IsWatchOnly bool + MasterPubParams []byte + MasterPrivParams []byte + EncryptedCryptoPubKey []byte + EncryptedCryptoPrivKey []byte + EncryptedCryptoScriptKey []byte + EncryptedMasterHdPubKey []byte + EncryptedMasterHdPrivKey []byte +} + +func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (int64, error) { + row := q.queryRow(ctx, q.createWalletStmt, CreateWallet, + arg.WalletName, + arg.ManagerVersion, + arg.ManagerCreatedAt, + arg.IsWatchOnly, + arg.MasterPubParams, + arg.MasterPrivParams, + arg.EncryptedCryptoPubKey, + arg.EncryptedCryptoPrivKey, + arg.EncryptedCryptoScriptKey, + arg.EncryptedMasterHdPubKey, + arg.EncryptedMasterHdPrivKey, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const GetWalletByName = `-- name: GetWalletByName :one +SELECT + id, + wallet_name, + manager_version, + manager_created_at, + is_watch_only, + master_pub_params, + master_priv_params, + encrypted_crypto_pub_key, + encrypted_crypto_priv_key, + encrypted_crypto_script_key, + encrypted_master_hd_pub_key, + encrypted_master_hd_priv_key +FROM wallets +WHERE wallet_name = $1 +` + +func (q *Queries) GetWalletByName(ctx context.Context, walletName string) (Wallet, error) { + row := q.queryRow(ctx, q.getWalletByNameStmt, GetWalletByName, walletName) + var i Wallet + err := row.Scan( + &i.ID, + &i.WalletName, + &i.ManagerVersion, + &i.ManagerCreatedAt, + &i.IsWatchOnly, + &i.MasterPubParams, + &i.MasterPrivParams, + &i.EncryptedCryptoPubKey, + &i.EncryptedCryptoPrivKey, + &i.EncryptedCryptoScriptKey, + &i.EncryptedMasterHdPubKey, + &i.EncryptedMasterHdPrivKey, + ) + return i, err +} + +const GetWalletSyncState = `-- name: GetWalletSyncState :one +SELECT + s.wallet_id, + s.start_block_height, + start_block.header_hash AS start_block_hash, + s.synced_block_height, + synced_block.header_hash AS synced_block_hash, + s.birthday_timestamp, + s.birthday_block_height, + birthday_block.header_hash AS birthday_block_hash, + s.birthday_block_verified +FROM wallet_sync_states AS s +INNER JOIN blocks AS start_block + ON s.start_block_height = start_block.block_height +INNER JOIN blocks AS synced_block + ON s.synced_block_height = synced_block.block_height +LEFT JOIN blocks AS birthday_block + ON s.birthday_block_height = birthday_block.block_height +WHERE s.wallet_id = $1 +` + +type GetWalletSyncStateRow struct { + WalletID int64 + StartBlockHeight int32 + StartBlockHash []byte + SyncedBlockHeight int32 + SyncedBlockHash []byte + BirthdayTimestamp int64 + BirthdayBlockHeight sql.NullInt32 + BirthdayBlockHash []byte + BirthdayBlockVerified bool +} + +func (q *Queries) GetWalletSyncState(ctx context.Context, walletID int64) (GetWalletSyncStateRow, error) { + row := q.queryRow(ctx, q.getWalletSyncStateStmt, GetWalletSyncState, walletID) + var i GetWalletSyncStateRow + err := row.Scan( + &i.WalletID, + &i.StartBlockHeight, + &i.StartBlockHash, + &i.SyncedBlockHeight, + &i.SyncedBlockHash, + &i.BirthdayTimestamp, + &i.BirthdayBlockHeight, + &i.BirthdayBlockHash, + &i.BirthdayBlockVerified, + ) + return i, err +} + +const PutWalletSyncState = `-- name: PutWalletSyncState :exec +INSERT INTO wallet_sync_states ( + wallet_id, + start_block_height, + synced_block_height, + birthday_timestamp, + birthday_block_height, + birthday_block_verified +) VALUES ($1, $2, $3, $4, $5, $6) +` + +type PutWalletSyncStateParams struct { + WalletID int64 + StartBlockHeight int32 + SyncedBlockHeight int32 + BirthdayTimestamp int64 + BirthdayBlockHeight sql.NullInt32 + BirthdayBlockVerified bool +} + +func (q *Queries) PutWalletSyncState(ctx context.Context, arg PutWalletSyncStateParams) error { + _, err := q.exec(ctx, q.putWalletSyncStateStmt, PutWalletSyncState, + arg.WalletID, + arg.StartBlockHeight, + arg.SyncedBlockHeight, + arg.BirthdayTimestamp, + arg.BirthdayBlockHeight, + arg.BirthdayBlockVerified, + ) + return err +} + +const UpdateWalletEncryption = `-- name: UpdateWalletEncryption :execrows +UPDATE wallets +SET + master_pub_params = $1, + master_priv_params = $2, + encrypted_crypto_pub_key = $3, + encrypted_crypto_priv_key = $4, + encrypted_crypto_script_key = $5, + encrypted_master_hd_pub_key = $6, + encrypted_master_hd_priv_key = $7 +WHERE id = $8 +` + +type UpdateWalletEncryptionParams struct { + MasterPubParams []byte + MasterPrivParams []byte + EncryptedCryptoPubKey []byte + EncryptedCryptoPrivKey []byte + EncryptedCryptoScriptKey []byte + EncryptedMasterHdPubKey []byte + EncryptedMasterHdPrivKey []byte + ID int64 +} + +func (q *Queries) UpdateWalletEncryption(ctx context.Context, arg UpdateWalletEncryptionParams) (int64, error) { + result, err := q.exec(ctx, q.updateWalletEncryptionStmt, UpdateWalletEncryption, + arg.MasterPubParams, + arg.MasterPrivParams, + arg.EncryptedCryptoPubKey, + arg.EncryptedCryptoPrivKey, + arg.EncryptedCryptoScriptKey, + arg.EncryptedMasterHdPubKey, + arg.EncryptedMasterHdPrivKey, + arg.ID, + ) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const UpdateWalletSyncState = `-- name: UpdateWalletSyncState :execrows +UPDATE wallet_sync_states +SET + start_block_height = $1, + synced_block_height = $2, + birthday_timestamp = $3, + birthday_block_height = $4, + birthday_block_verified = $5 +WHERE wallet_id = $6 +` + +type UpdateWalletSyncStateParams struct { + StartBlockHeight int32 + SyncedBlockHeight int32 + BirthdayTimestamp int64 + BirthdayBlockHeight sql.NullInt32 + BirthdayBlockVerified bool + WalletID int64 +} + +func (q *Queries) UpdateWalletSyncState(ctx context.Context, arg UpdateWalletSyncStateParams) (int64, error) { + result, err := q.exec(ctx, q.updateWalletSyncStateStmt, UpdateWalletSyncState, + arg.StartBlockHeight, + arg.SyncedBlockHeight, + arg.BirthdayTimestamp, + arg.BirthdayBlockHeight, + arg.BirthdayBlockVerified, + arg.WalletID, + ) + if err != nil { + return 0, err + } + return result.RowsAffected() +} diff --git a/wallet/internal/sql/sqlite/legacy_schema_test.go b/wallet/internal/sql/sqlite/legacy_schema_test.go new file mode 100644 index 0000000000..b27361d6f2 --- /dev/null +++ b/wallet/internal/sql/sqlite/legacy_schema_test.go @@ -0,0 +1,297 @@ +package sqlite + +import ( + "context" + "crypto/sha256" + "database/sql" + "path/filepath" + "testing" + + "github.com/btcsuite/btcwallet/wallet/internal/sql/sqlite/sqlc" + "github.com/stretchr/testify/require" +) + +// TestLegacySchemaQueries verifies that the SQL shape can represent the legacy +// wallet, scope, account, and address records without decrypting them. +func TestLegacySchemaQueries(t *testing.T) { + t.Parallel() + + ctx := context.Background() + db, err := Open(ctx, Config{ + DBPath: filepath.Join(t.TempDir(), "wallet.db"), + }) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + require.NoError(t, ApplyMigrations(db)) + requireNoPlaintextColumns(t, db) + + queries := sqlc.New(db) + genesisHash := bytesOf(0x01, 32) + birthdayHash := bytesOf(0x02, 32) + + require.NoError(t, queries.InsertBlock(ctx, sqlc.InsertBlockParams{ + BlockHeight: 0, + HeaderHash: genesisHash, + BlockTimestamp: 1, + })) + require.NoError(t, queries.InsertBlock(ctx, sqlc.InsertBlockParams{ + BlockHeight: 100, + HeaderHash: birthdayHash, + BlockTimestamp: 2, + })) + + walletID, err := queries.CreateWallet(ctx, sqlc.CreateWalletParams{ + WalletName: "watch-only", + ManagerVersion: 8, + ManagerCreatedAt: 123, + IsWatchOnly: true, + MasterPubParams: []byte("encrypted-master-public-params"), + EncryptedCryptoPubKey: []byte("encrypted-crypto-public-key"), + }) + require.NoError(t, err) + + wallet, err := queries.GetWalletByName(ctx, "watch-only") + require.NoError(t, err) + require.Equal(t, int64(123), wallet.ManagerCreatedAt) + require.Equal(t, []byte("encrypted-master-public-params"), + wallet.MasterPubParams) + require.Nil(t, wallet.MasterPrivParams) + require.Equal(t, []byte("encrypted-crypto-public-key"), + wallet.EncryptedCryptoPubKey) + require.Nil(t, wallet.EncryptedCryptoPrivKey) + require.Nil(t, wallet.EncryptedCryptoScriptKey) + + require.NoError(t, queries.PutWalletSyncState( + ctx, sqlc.PutWalletSyncStateParams{ + WalletID: walletID, + StartBlockHeight: 0, + SyncedBlockHeight: 100, + BirthdayTimestamp: 1234, + BirthdayBlockHeight: sql.NullInt64{Int64: 100, Valid: true}, + BirthdayBlockVerified: true, + }, + )) + syncState, err := queries.GetWalletSyncState(ctx, walletID) + require.NoError(t, err) + require.Equal(t, genesisHash, syncState.StartBlockHash) + require.Equal(t, birthdayHash, syncState.SyncedBlockHash) + require.Equal(t, birthdayHash, syncState.BirthdayBlockHash) + + scopeID, err := queries.CreateKeyScope(ctx, sqlc.CreateKeyScopeParams{ + WalletID: walletID, + Purpose: 84, + CoinType: 0, + ExternalAddrType: 4, + InternalAddrType: 4, + }) + require.NoError(t, err) + scope, err := queries.GetKeyScope(ctx, sqlc.GetKeyScopeParams{ + WalletID: walletID, + Purpose: 84, + CoinType: 0, + }) + require.NoError(t, err) + require.Nil(t, scope.EncryptedCoinPubKey) + require.Nil(t, scope.EncryptedCoinPrivKey) + require.False(t, scope.LastAccountNumber.Valid) + rows, err := queries.UpdateLastAccountNumber( + ctx, sqlc.UpdateLastAccountNumberParams{ + LastAccountNumber: sql.NullInt64{Int64: 7, Valid: true}, + ID: scopeID, + WalletID: walletID, + }, + ) + require.NoError(t, err) + require.Equal(t, int64(1), rows) + + scope, err = queries.GetKeyScope(ctx, sqlc.GetKeyScopeParams{ + WalletID: walletID, + Purpose: 84, + CoinType: 0, + }) + require.NoError(t, err) + require.Equal(t, int64(7), scope.LastAccountNumber.Int64) + + err = queries.CreateAccount(ctx, sqlc.CreateAccountParams{ + WalletID: walletID, + ScopeID: scopeID, + AccountNumber: 7, + AccountType: 1, + AccountName: "imported", + EncryptedPubKey: []byte("encrypted-account-public-key"), + MasterKeyFingerprint: sql.NullInt64{Int64: 42, Valid: true}, + ExternalAddrType: sql.NullInt64{Int64: 3, Valid: true}, + InternalAddrType: sql.NullInt64{Int64: 4, Valid: true}, + }) + require.NoError(t, err) + account, err := queries.GetAccount(ctx, sqlc.GetAccountParams{ + ScopeID: scopeID, + AccountNumber: 7, + }) + require.NoError(t, err) + require.Equal(t, int64(1), account.AccountType) + require.Equal(t, []byte("encrypted-account-public-key"), + account.EncryptedPubKey) + require.Nil(t, account.EncryptedPrivKey) + require.Equal(t, int64(42), account.MasterKeyFingerprint.Int64) + + createLegacyAddressRows(t, ctx, db, queries, walletID, scopeID) +} + +func requireNoPlaintextColumns(t *testing.T, db *sql.DB) { + t.Helper() + + columns := map[string][]string{ + "wallets": {"master_hd_pub_key"}, + "key_scopes": {"coin_pub_key", "coin_priv_key"}, + "accounts": {"public_key", "private_key"}, + "addresses": {"address_id", "script_pub_key", "pub_key", "script"}, + } + for table, tableColumns := range columns { + for _, column := range tableColumns { + var count int + + err := db.QueryRowContext( + context.Background(), + "SELECT count(*) FROM pragma_table_info(?) WHERE name = ?", + table, column, + ).Scan(&count) + require.NoError(t, err) + require.Zero(t, count, "%s.%s", table, column) + } + } +} + +func createLegacyAddressRows(t *testing.T, ctx context.Context, db *sql.DB, + queries *sqlc.Queries, walletID, scopeID int64) { + + t.Helper() + + chainHash := sha256.Sum256([]byte("chain-address-id")) + require.NoError(t, queries.CreateAddress(ctx, sqlc.CreateAddressParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: chainHash[:], + AccountNumber: 7, + AddressType: 0, + AddedAt: 10, + SyncStatus: 2, + Branch: sql.NullInt64{Int64: 0, Valid: true}, + AddressIndex: sql.NullInt64{Int64: 9, Valid: true}, + })) + + importedHash := sha256.Sum256([]byte("imported-address-id")) + require.NoError(t, queries.CreateAddress(ctx, sqlc.CreateAddressParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: importedHash[:], + AccountNumber: 7, + AddressType: 1, + AddedAt: 11, + SyncStatus: 2, + EncryptedPubKey: []byte("encrypted-imported-public-key"), + EncryptedPrivKey: []byte{}, + })) + + scriptHash := sha256.Sum256([]byte("script-address-id")) + require.NoError(t, queries.CreateAddress(ctx, sqlc.CreateAddressParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: scriptHash[:], + AccountNumber: 7, + AddressType: 2, + AddedAt: 12, + SyncStatus: 2, + EncryptedHash: []byte("encrypted-script-hash"), + EncryptedScript: []byte("encrypted-script"), + })) + + watchOnlyScriptHash := sha256.Sum256([]byte("watch-only-script-address-id")) + require.NoError(t, queries.CreateAddress(ctx, sqlc.CreateAddressParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: watchOnlyScriptHash[:], + AccountNumber: 7, + AddressType: 2, + AddedAt: 12, + SyncStatus: 2, + EncryptedHash: []byte("encrypted-watch-only-script-hash"), + })) + + witnessHash := sha256.Sum256([]byte("witness-address-id")) + require.NoError(t, queries.CreateAddress(ctx, sqlc.CreateAddressParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: witnessHash[:], + AccountNumber: 7, + AddressType: 3, + AddedAt: 13, + SyncStatus: 2, + EncryptedHash: []byte("encrypted-witness-hash"), + EncryptedScript: []byte("encrypted-witness-script"), + WitnessVersion: sql.NullInt64{Int64: 0, Valid: true}, + IsSecretScript: sql.NullBool{Bool: true, Valid: true}, + })) + + taprootHash := sha256.Sum256([]byte("taproot-address-id")) + require.NoError(t, queries.CreateAddress(ctx, sqlc.CreateAddressParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: taprootHash[:], + AccountNumber: 7, + AddressType: 4, + AddedAt: 14, + SyncStatus: 2, + EncryptedHash: []byte("encrypted-taproot-hash"), + EncryptedScript: []byte("encrypted-taproot-script"), + WitnessVersion: sql.NullInt64{Int64: 1, Valid: true}, + IsSecretScript: sql.NullBool{Bool: false, Valid: true}, + })) + + rows, err := queries.MarkAddressUsed(ctx, sqlc.MarkAddressUsedParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: importedHash[:], + }) + require.NoError(t, err) + require.Equal(t, int64(1), rows) + + address, err := queries.GetAddress(ctx, sqlc.GetAddressParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: importedHash[:], + }) + require.NoError(t, err) + require.True(t, address.Used) + require.Equal(t, []byte("encrypted-imported-public-key"), + address.EncryptedPubKey) + + _, err = db.ExecContext(ctx, ` + UPDATE addresses SET used = FALSE + WHERE wallet_id = ? AND scope_id = ? AND address_hash = ? + `, walletID, scopeID, importedHash[:]) + require.ErrorContains(t, err, "address used state cannot be cleared") + + invalidHash := sha256.Sum256([]byte("invalid-chain-address-id")) + err = queries.CreateAddress(ctx, sqlc.CreateAddressParams{ + WalletID: walletID, + ScopeID: scopeID, + AddressHash: invalidHash[:], + AccountNumber: 7, + AddressType: 0, + AddedAt: 15, + SyncStatus: 2, + }) + require.Error(t, err) +} + +func bytesOf(value byte, count int) []byte { + b := make([]byte, count) + for i := range b { + b[i] = value + } + + return b +} diff --git a/wallet/internal/sql/sqlite/migrations/000002_wallets.down.sql b/wallet/internal/sql/sqlite/migrations/000002_wallets.down.sql new file mode 100644 index 0000000000..0b73d6e376 --- /dev/null +++ b/wallet/internal/sql/sqlite/migrations/000002_wallets.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS wallet_sync_states; +DROP TABLE IF EXISTS wallets; diff --git a/wallet/internal/sql/sqlite/migrations/000002_wallets.up.sql b/wallet/internal/sql/sqlite/migrations/000002_wallets.up.sql new file mode 100644 index 0000000000..f7f538f555 --- /dev/null +++ b/wallet/internal/sql/sqlite/migrations/000002_wallets.up.sql @@ -0,0 +1,39 @@ +-- The wallets table preserves the legacy waddrmgr encryption hierarchy. +CREATE TABLE wallets ( + id INTEGER PRIMARY KEY, + wallet_name TEXT NOT NULL UNIQUE, + manager_version INTEGER NOT NULL + CHECK (manager_version BETWEEN 0 AND 4294967295), + manager_created_at INTEGER NOT NULL CHECK (manager_created_at >= 0), + is_watch_only BOOLEAN NOT NULL CHECK (is_watch_only IN (FALSE, TRUE)), + master_pub_params BLOB NOT NULL, + master_priv_params BLOB, + encrypted_crypto_pub_key BLOB NOT NULL, + encrypted_crypto_priv_key BLOB, + encrypted_crypto_script_key BLOB, + encrypted_master_hd_pub_key BLOB, + encrypted_master_hd_priv_key BLOB +); + +-- The wallet sync state mirrors the block stamps and birthday metadata stored +-- by the legacy address manager. +CREATE TABLE wallet_sync_states ( + wallet_id INTEGER PRIMARY KEY, + start_block_height INTEGER NOT NULL, + synced_block_height INTEGER NOT NULL, + birthday_timestamp INTEGER NOT NULL CHECK (birthday_timestamp >= 0), + birthday_block_height INTEGER, + birthday_block_verified BOOLEAN NOT NULL DEFAULT FALSE + CHECK (birthday_block_verified IN (FALSE, TRUE)), + FOREIGN KEY (wallet_id) REFERENCES wallets (id) ON DELETE RESTRICT, + FOREIGN KEY (start_block_height) REFERENCES blocks (block_height) + ON DELETE RESTRICT, + FOREIGN KEY (synced_block_height) REFERENCES blocks (block_height) + ON DELETE RESTRICT, + FOREIGN KEY (birthday_block_height) REFERENCES blocks (block_height) + ON DELETE RESTRICT, + CHECK ( + birthday_block_verified = FALSE + OR birthday_block_height IS NOT NULL + ) +); diff --git a/wallet/internal/sql/sqlite/migrations/000003_address_types.down.sql b/wallet/internal/sql/sqlite/migrations/000003_address_types.down.sql new file mode 100644 index 0000000000..a8937217d1 --- /dev/null +++ b/wallet/internal/sql/sqlite/migrations/000003_address_types.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS address_types; diff --git a/wallet/internal/sql/sqlite/migrations/000003_address_types.up.sql b/wallet/internal/sql/sqlite/migrations/000003_address_types.up.sql new file mode 100644 index 0000000000..4a16f15e3c --- /dev/null +++ b/wallet/internal/sql/sqlite/migrations/000003_address_types.up.sql @@ -0,0 +1,16 @@ +-- Address types are the stable public waddrmgr.AddressType values used by key +-- scope address schemas. +CREATE TABLE address_types ( + id INTEGER PRIMARY KEY CHECK (id BETWEEN 0 AND 7), + type_name TEXT NOT NULL UNIQUE +); + +INSERT INTO address_types (id, type_name) VALUES + (0, 'pubkey-hash'), + (1, 'script'), + (2, 'raw-pubkey'), + (3, 'nested-witness-pubkey'), + (4, 'witness-pubkey'), + (5, 'witness-script'), + (6, 'taproot-pubkey'), + (7, 'taproot-script'); diff --git a/wallet/internal/sql/sqlite/migrations/000004_key_scopes.down.sql b/wallet/internal/sql/sqlite/migrations/000004_key_scopes.down.sql new file mode 100644 index 0000000000..ae9bf8f8bc --- /dev/null +++ b/wallet/internal/sql/sqlite/migrations/000004_key_scopes.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS key_scopes; diff --git a/wallet/internal/sql/sqlite/migrations/000004_key_scopes.up.sql b/wallet/internal/sql/sqlite/migrations/000004_key_scopes.up.sql new file mode 100644 index 0000000000..7f8579a9b1 --- /dev/null +++ b/wallet/internal/sql/sqlite/migrations/000004_key_scopes.up.sql @@ -0,0 +1,21 @@ +-- Key scopes preserve encrypted coin keys and the address schema associated +-- with each purpose and coin tuple. +CREATE TABLE key_scopes ( + id INTEGER PRIMARY KEY, + wallet_id INTEGER NOT NULL, + purpose INTEGER NOT NULL CHECK (purpose BETWEEN 0 AND 4294967295), + coin_type INTEGER NOT NULL CHECK (coin_type BETWEEN 0 AND 4294967295), + encrypted_coin_pub_key BLOB, + encrypted_coin_priv_key BLOB, + last_account_number INTEGER + CHECK (last_account_number BETWEEN 0 AND 4294967295), + external_addr_type INTEGER NOT NULL, + internal_addr_type INTEGER NOT NULL, + FOREIGN KEY (wallet_id) REFERENCES wallets (id) ON DELETE RESTRICT, + FOREIGN KEY (external_addr_type) REFERENCES address_types (id) + ON DELETE RESTRICT, + FOREIGN KEY (internal_addr_type) REFERENCES address_types (id) + ON DELETE RESTRICT, + UNIQUE (wallet_id, purpose, coin_type), + UNIQUE (id, wallet_id) +); diff --git a/wallet/internal/sql/sqlite/migrations/000005_accounts.down.sql b/wallet/internal/sql/sqlite/migrations/000005_accounts.down.sql new file mode 100644 index 0000000000..1616db4979 --- /dev/null +++ b/wallet/internal/sql/sqlite/migrations/000005_accounts.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS accounts; diff --git a/wallet/internal/sql/sqlite/migrations/000005_accounts.up.sql b/wallet/internal/sql/sqlite/migrations/000005_accounts.up.sql new file mode 100644 index 0000000000..b21968cc84 --- /dev/null +++ b/wallet/internal/sql/sqlite/migrations/000005_accounts.up.sql @@ -0,0 +1,47 @@ +-- Accounts preserve the two stable legacy account encodings. Every account, +-- including an imported watch-only account, retains its uint32 account number. +CREATE TABLE accounts ( + wallet_id INTEGER NOT NULL, + scope_id INTEGER NOT NULL, + account_number INTEGER NOT NULL + CHECK (account_number BETWEEN 0 AND 4294967295), + account_type INTEGER NOT NULL CHECK (account_type IN (0, 1)), + account_name TEXT NOT NULL, + encrypted_pub_key BLOB NOT NULL, + encrypted_priv_key BLOB, + master_key_fingerprint INTEGER + CHECK ( + master_key_fingerprint IS NULL + OR master_key_fingerprint BETWEEN 0 AND 4294967295 + ), + next_external_index INTEGER NOT NULL DEFAULT 0 + CHECK (next_external_index BETWEEN 0 AND 4294967295), + next_internal_index INTEGER NOT NULL DEFAULT 0 + CHECK (next_internal_index BETWEEN 0 AND 4294967295), + external_addr_type INTEGER, + internal_addr_type INTEGER, + FOREIGN KEY (scope_id, wallet_id) REFERENCES key_scopes (id, wallet_id) + ON DELETE RESTRICT, + FOREIGN KEY (external_addr_type) REFERENCES address_types (id) + ON DELETE RESTRICT, + FOREIGN KEY (internal_addr_type) REFERENCES address_types (id) + ON DELETE RESTRICT, + PRIMARY KEY (scope_id, account_number), + UNIQUE (scope_id, account_name), + UNIQUE (scope_id, account_number, wallet_id), + CHECK ( + (external_addr_type IS NULL AND internal_addr_type IS NULL) + OR (external_addr_type IS NOT NULL AND internal_addr_type IS NOT NULL) + ), + CHECK ( + (account_type = 0 + AND master_key_fingerprint IS NULL + AND external_addr_type IS NULL + AND internal_addr_type IS NULL) + OR ( + account_type = 1 + AND encrypted_priv_key IS NULL + AND master_key_fingerprint IS NOT NULL + ) + ) +); diff --git a/wallet/internal/sql/sqlite/migrations/000006_addresses.down.sql b/wallet/internal/sql/sqlite/migrations/000006_addresses.down.sql new file mode 100644 index 0000000000..9a3cd20201 --- /dev/null +++ b/wallet/internal/sql/sqlite/migrations/000006_addresses.down.sql @@ -0,0 +1,2 @@ +DROP TRIGGER IF EXISTS trg_addresses_used_is_monotonic; +DROP TABLE IF EXISTS addresses; diff --git a/wallet/internal/sql/sqlite/migrations/000006_addresses.up.sql b/wallet/internal/sql/sqlite/migrations/000006_addresses.up.sql new file mode 100644 index 0000000000..329cac2492 --- /dev/null +++ b/wallet/internal/sql/sqlite/migrations/000006_addresses.up.sql @@ -0,0 +1,73 @@ +-- Addresses are keyed by SHA256(addressID), matching the legacy address and +-- used-address buckets without exposing the encoded address in plaintext. +CREATE TABLE addresses ( + wallet_id INTEGER NOT NULL, + scope_id INTEGER NOT NULL, + address_hash BLOB NOT NULL CHECK (length(address_hash) = 32), + account_number INTEGER NOT NULL + CHECK (account_number BETWEEN 0 AND 4294967295), + address_type INTEGER NOT NULL CHECK (address_type BETWEEN 0 AND 4), + added_at INTEGER NOT NULL CHECK (added_at >= 0), + sync_status INTEGER NOT NULL CHECK (sync_status IN (0, 1, 2)), + branch INTEGER CHECK (branch BETWEEN 0 AND 4294967295), + address_index INTEGER CHECK (address_index BETWEEN 0 AND 4294967295), + encrypted_pub_key BLOB, + encrypted_priv_key BLOB, + encrypted_hash BLOB, + encrypted_script BLOB, + witness_version INTEGER CHECK (witness_version BETWEEN 0 AND 255), + is_secret_script BOOLEAN + CHECK (is_secret_script IS NULL OR is_secret_script IN (FALSE, TRUE)), + used BOOLEAN NOT NULL DEFAULT FALSE CHECK (used IN (FALSE, TRUE)), + PRIMARY KEY (wallet_id, scope_id, address_hash), + FOREIGN KEY (scope_id, account_number, wallet_id) + REFERENCES accounts (scope_id, account_number, wallet_id) + ON DELETE RESTRICT, + CHECK ( + (address_type = 0 + AND branch IS NOT NULL + AND address_index IS NOT NULL + AND encrypted_pub_key IS NULL + AND encrypted_priv_key IS NULL + AND encrypted_hash IS NULL + AND encrypted_script IS NULL + AND witness_version IS NULL + AND is_secret_script IS NULL) + OR (address_type = 1 + AND branch IS NULL + AND address_index IS NULL + AND encrypted_pub_key IS NOT NULL + AND encrypted_hash IS NULL + AND encrypted_script IS NULL + AND witness_version IS NULL + AND is_secret_script IS NULL) + OR (address_type = 2 + AND branch IS NULL + AND address_index IS NULL + AND encrypted_pub_key IS NULL + AND encrypted_priv_key IS NULL + AND encrypted_hash IS NOT NULL + AND witness_version IS NULL + AND is_secret_script IS NULL) + OR (address_type IN (3, 4) + AND branch IS NULL + AND address_index IS NULL + AND encrypted_pub_key IS NULL + AND encrypted_priv_key IS NULL + AND encrypted_hash IS NOT NULL + AND witness_version IS NOT NULL + AND is_secret_script IS NOT NULL) + ) +); + +CREATE INDEX idx_addresses_account +ON addresses (scope_id, account_number, address_hash); + +-- The used bit is sticky, as it is in the legacy used-address bucket. +CREATE TRIGGER trg_addresses_used_is_monotonic +BEFORE UPDATE OF used ON addresses +FOR EACH ROW +WHEN old.used = TRUE AND new.used = FALSE +BEGIN + SELECT raise(ABORT, 'address used state cannot be cleared'); +END; diff --git a/wallet/internal/sql/sqlite/migrations_test.go b/wallet/internal/sql/sqlite/migrations_test.go index 96f1ee2600..91adc0abd6 100644 --- a/wallet/internal/sql/sqlite/migrations_test.go +++ b/wallet/internal/sql/sqlite/migrations_test.go @@ -23,25 +23,31 @@ func TestMigrationsRoundTrip(t *testing.T) { }) require.NoError(t, ApplyMigrations(db)) - requireBlocksTable(t, db, true) + requireSchemaTables(t, db, true) require.NoError(t, RollbackMigrations(db)) - requireBlocksTable(t, db, false) + requireSchemaTables(t, db, false) require.NoError(t, ApplyMigrations(db)) - requireBlocksTable(t, db, true) + requireSchemaTables(t, db, true) } -func requireBlocksTable(t *testing.T, db *sql.DB, expected bool) { +func requireSchemaTables(t *testing.T, db *sql.DB, expected bool) { t.Helper() - var count int - - err := db.QueryRowContext( - context.Background(), - "SELECT count(*) FROM sqlite_master "+ - "WHERE type = 'table' AND name = 'blocks'", - ).Scan(&count) - require.NoError(t, err) - require.Equal(t, expected, count == 1) + tables := []string{ + "blocks", "wallets", "wallet_sync_states", "address_types", + "key_scopes", "accounts", "addresses", + } + for _, table := range tables { + var count int + + err := db.QueryRowContext( + context.Background(), + "SELECT count(*) FROM sqlite_master "+ + "WHERE type = 'table' AND name = ?", table, + ).Scan(&count) + require.NoError(t, err) + require.Equal(t, expected, count == 1, table) + } } diff --git a/wallet/internal/sql/sqlite/queries/accounts.sql b/wallet/internal/sql/sqlite/queries/accounts.sql new file mode 100644 index 0000000000..e92c6148f8 --- /dev/null +++ b/wallet/internal/sql/sqlite/queries/accounts.sql @@ -0,0 +1,60 @@ +-- name: CreateAccount :exec +INSERT INTO accounts ( + wallet_id, + scope_id, + account_number, + account_type, + account_name, + encrypted_pub_key, + encrypted_priv_key, + master_key_fingerprint, + next_external_index, + next_internal_index, + external_addr_type, + internal_addr_type +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + +-- name: GetAccount :one +SELECT + wallet_id, + scope_id, + account_number, + account_type, + account_name, + encrypted_pub_key, + encrypted_priv_key, + master_key_fingerprint, + next_external_index, + next_internal_index, + external_addr_type, + internal_addr_type +FROM accounts +WHERE scope_id = ? AND account_number = ?; + +-- name: ListAccounts :many +SELECT + wallet_id, + scope_id, + account_number, + account_type, + account_name, + encrypted_pub_key, + encrypted_priv_key, + master_key_fingerprint, + next_external_index, + next_internal_index, + external_addr_type, + internal_addr_type +FROM accounts +WHERE scope_id = ? +ORDER BY account_number; + +-- name: RenameAccount :execrows +UPDATE accounts +SET account_name = ? +WHERE scope_id = ? AND account_number = ?; + +-- name: UpdateAccountIndexes :execrows +UPDATE accounts +SET next_external_index = ?, next_internal_index = ? +WHERE scope_id = ? AND account_number = ?; diff --git a/wallet/internal/sql/sqlite/queries/address_types.sql b/wallet/internal/sql/sqlite/queries/address_types.sql new file mode 100644 index 0000000000..879c9df0e2 --- /dev/null +++ b/wallet/internal/sql/sqlite/queries/address_types.sql @@ -0,0 +1,4 @@ +-- name: ListAddressTypes :many +SELECT id, type_name +FROM address_types +ORDER BY id; diff --git a/wallet/internal/sql/sqlite/queries/addresses.sql b/wallet/internal/sql/sqlite/queries/addresses.sql new file mode 100644 index 0000000000..ea1ec35b7a --- /dev/null +++ b/wallet/internal/sql/sqlite/queries/addresses.sql @@ -0,0 +1,67 @@ +-- name: CreateAddress :exec +INSERT INTO addresses ( + wallet_id, + scope_id, + address_hash, + account_number, + address_type, + added_at, + sync_status, + branch, + address_index, + encrypted_pub_key, + encrypted_priv_key, + encrypted_hash, + encrypted_script, + witness_version, + is_secret_script, + used +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + +-- name: GetAddress :one +SELECT + wallet_id, + scope_id, + address_hash, + account_number, + address_type, + added_at, + sync_status, + branch, + address_index, + encrypted_pub_key, + encrypted_priv_key, + encrypted_hash, + encrypted_script, + witness_version, + is_secret_script, + used +FROM addresses +WHERE wallet_id = ? AND scope_id = ? AND address_hash = ?; + +-- name: ListAccountAddresses :many +SELECT + wallet_id, + scope_id, + address_hash, + account_number, + address_type, + added_at, + sync_status, + branch, + address_index, + encrypted_pub_key, + encrypted_priv_key, + encrypted_hash, + encrypted_script, + witness_version, + is_secret_script, + used +FROM addresses +WHERE scope_id = ? AND account_number = ? +ORDER BY address_hash; + +-- name: MarkAddressUsed :execrows +UPDATE addresses +SET used = TRUE +WHERE wallet_id = ? AND scope_id = ? AND address_hash = ?; diff --git a/wallet/internal/sql/sqlite/queries/key_scopes.sql b/wallet/internal/sql/sqlite/queries/key_scopes.sql new file mode 100644 index 0000000000..d6f0519dbb --- /dev/null +++ b/wallet/internal/sql/sqlite/queries/key_scopes.sql @@ -0,0 +1,37 @@ +-- name: CreateKeyScope :one +INSERT INTO key_scopes ( + wallet_id, + purpose, + coin_type, + encrypted_coin_pub_key, + encrypted_coin_priv_key, + external_addr_type, + internal_addr_type +) VALUES (?, ?, ?, ?, ?, ?, ?) +RETURNING id; + +-- name: GetKeyScope :one +SELECT + id, + wallet_id, + purpose, + coin_type, + encrypted_coin_pub_key, + encrypted_coin_priv_key, + last_account_number, + external_addr_type, + internal_addr_type +FROM key_scopes +WHERE wallet_id = ? AND purpose = ? AND coin_type = ?; + +-- name: UpdateKeyScopeKeys :execrows +UPDATE key_scopes +SET + encrypted_coin_pub_key = ?, + encrypted_coin_priv_key = ? +WHERE id = ? AND wallet_id = ?; + +-- name: UpdateLastAccountNumber :execrows +UPDATE key_scopes +SET last_account_number = ? +WHERE id = ? AND wallet_id = ?; diff --git a/wallet/internal/sql/sqlite/queries/wallets.sql b/wallet/internal/sql/sqlite/queries/wallets.sql new file mode 100644 index 0000000000..bffc499171 --- /dev/null +++ b/wallet/internal/sql/sqlite/queries/wallets.sql @@ -0,0 +1,84 @@ +-- name: CreateWallet :one +INSERT INTO wallets ( + wallet_name, + manager_version, + manager_created_at, + is_watch_only, + master_pub_params, + master_priv_params, + encrypted_crypto_pub_key, + encrypted_crypto_priv_key, + encrypted_crypto_script_key, + encrypted_master_hd_pub_key, + encrypted_master_hd_priv_key +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +RETURNING id; + +-- name: GetWalletByName :one +SELECT + id, + wallet_name, + manager_version, + manager_created_at, + is_watch_only, + master_pub_params, + master_priv_params, + encrypted_crypto_pub_key, + encrypted_crypto_priv_key, + encrypted_crypto_script_key, + encrypted_master_hd_pub_key, + encrypted_master_hd_priv_key +FROM wallets +WHERE wallet_name = ?; + +-- name: UpdateWalletEncryption :execrows +UPDATE wallets +SET + master_pub_params = ?, + master_priv_params = ?, + encrypted_crypto_pub_key = ?, + encrypted_crypto_priv_key = ?, + encrypted_crypto_script_key = ?, + encrypted_master_hd_pub_key = ?, + encrypted_master_hd_priv_key = ? +WHERE id = ?; + +-- name: PutWalletSyncState :exec +INSERT INTO wallet_sync_states ( + wallet_id, + start_block_height, + synced_block_height, + birthday_timestamp, + birthday_block_height, + birthday_block_verified +) VALUES (?, ?, ?, ?, ?, ?); + +-- name: GetWalletSyncState :one +SELECT + s.wallet_id, + s.start_block_height, + start_block.header_hash AS start_block_hash, + s.synced_block_height, + synced_block.header_hash AS synced_block_hash, + s.birthday_timestamp, + s.birthday_block_height, + birthday_block.header_hash AS birthday_block_hash, + s.birthday_block_verified +FROM wallet_sync_states AS s +INNER JOIN blocks AS start_block + ON s.start_block_height = start_block.block_height +INNER JOIN blocks AS synced_block + ON s.synced_block_height = synced_block.block_height +LEFT JOIN blocks AS birthday_block + ON s.birthday_block_height = birthday_block.block_height +WHERE s.wallet_id = ?; + +-- name: UpdateWalletSyncState :execrows +UPDATE wallet_sync_states +SET + start_block_height = ?, + synced_block_height = ?, + birthday_timestamp = ?, + birthday_block_height = ?, + birthday_block_verified = ? +WHERE wallet_id = ?; diff --git a/wallet/internal/sql/sqlite/sqlc/accounts.sql.go b/wallet/internal/sql/sqlite/sqlc/accounts.sql.go new file mode 100644 index 0000000000..5c892c3769 --- /dev/null +++ b/wallet/internal/sql/sqlite/sqlc/accounts.sql.go @@ -0,0 +1,205 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: accounts.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const CreateAccount = `-- name: CreateAccount :exec +INSERT INTO accounts ( + wallet_id, + scope_id, + account_number, + account_type, + account_name, + encrypted_pub_key, + encrypted_priv_key, + master_key_fingerprint, + next_external_index, + next_internal_index, + external_addr_type, + internal_addr_type +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +` + +type CreateAccountParams struct { + WalletID int64 + ScopeID int64 + AccountNumber int64 + AccountType int64 + AccountName string + EncryptedPubKey []byte + EncryptedPrivKey []byte + MasterKeyFingerprint sql.NullInt64 + NextExternalIndex int64 + NextInternalIndex int64 + ExternalAddrType sql.NullInt64 + InternalAddrType sql.NullInt64 +} + +func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) error { + _, err := q.exec(ctx, q.createAccountStmt, CreateAccount, + arg.WalletID, + arg.ScopeID, + arg.AccountNumber, + arg.AccountType, + arg.AccountName, + arg.EncryptedPubKey, + arg.EncryptedPrivKey, + arg.MasterKeyFingerprint, + arg.NextExternalIndex, + arg.NextInternalIndex, + arg.ExternalAddrType, + arg.InternalAddrType, + ) + return err +} + +const GetAccount = `-- name: GetAccount :one +SELECT + wallet_id, + scope_id, + account_number, + account_type, + account_name, + encrypted_pub_key, + encrypted_priv_key, + master_key_fingerprint, + next_external_index, + next_internal_index, + external_addr_type, + internal_addr_type +FROM accounts +WHERE scope_id = ? AND account_number = ? +` + +type GetAccountParams struct { + ScopeID int64 + AccountNumber int64 +} + +func (q *Queries) GetAccount(ctx context.Context, arg GetAccountParams) (Account, error) { + row := q.queryRow(ctx, q.getAccountStmt, GetAccount, arg.ScopeID, arg.AccountNumber) + var i Account + err := row.Scan( + &i.WalletID, + &i.ScopeID, + &i.AccountNumber, + &i.AccountType, + &i.AccountName, + &i.EncryptedPubKey, + &i.EncryptedPrivKey, + &i.MasterKeyFingerprint, + &i.NextExternalIndex, + &i.NextInternalIndex, + &i.ExternalAddrType, + &i.InternalAddrType, + ) + return i, err +} + +const ListAccounts = `-- name: ListAccounts :many +SELECT + wallet_id, + scope_id, + account_number, + account_type, + account_name, + encrypted_pub_key, + encrypted_priv_key, + master_key_fingerprint, + next_external_index, + next_internal_index, + external_addr_type, + internal_addr_type +FROM accounts +WHERE scope_id = ? +ORDER BY account_number +` + +func (q *Queries) ListAccounts(ctx context.Context, scopeID int64) ([]Account, error) { + rows, err := q.query(ctx, q.listAccountsStmt, ListAccounts, scopeID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Account + for rows.Next() { + var i Account + if err := rows.Scan( + &i.WalletID, + &i.ScopeID, + &i.AccountNumber, + &i.AccountType, + &i.AccountName, + &i.EncryptedPubKey, + &i.EncryptedPrivKey, + &i.MasterKeyFingerprint, + &i.NextExternalIndex, + &i.NextInternalIndex, + &i.ExternalAddrType, + &i.InternalAddrType, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const RenameAccount = `-- name: RenameAccount :execrows +UPDATE accounts +SET account_name = ? +WHERE scope_id = ? AND account_number = ? +` + +type RenameAccountParams struct { + AccountName string + ScopeID int64 + AccountNumber int64 +} + +func (q *Queries) RenameAccount(ctx context.Context, arg RenameAccountParams) (int64, error) { + result, err := q.exec(ctx, q.renameAccountStmt, RenameAccount, arg.AccountName, arg.ScopeID, arg.AccountNumber) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const UpdateAccountIndexes = `-- name: UpdateAccountIndexes :execrows +UPDATE accounts +SET next_external_index = ?, next_internal_index = ? +WHERE scope_id = ? AND account_number = ? +` + +type UpdateAccountIndexesParams struct { + NextExternalIndex int64 + NextInternalIndex int64 + ScopeID int64 + AccountNumber int64 +} + +func (q *Queries) UpdateAccountIndexes(ctx context.Context, arg UpdateAccountIndexesParams) (int64, error) { + result, err := q.exec(ctx, q.updateAccountIndexesStmt, UpdateAccountIndexes, + arg.NextExternalIndex, + arg.NextInternalIndex, + arg.ScopeID, + arg.AccountNumber, + ) + if err != nil { + return 0, err + } + return result.RowsAffected() +} diff --git a/wallet/internal/sql/sqlite/sqlc/address_types.sql.go b/wallet/internal/sql/sqlite/sqlc/address_types.sql.go new file mode 100644 index 0000000000..54b21a9a9f --- /dev/null +++ b/wallet/internal/sql/sqlite/sqlc/address_types.sql.go @@ -0,0 +1,39 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: address_types.sql + +package sqlc + +import ( + "context" +) + +const ListAddressTypes = `-- name: ListAddressTypes :many +SELECT id, type_name +FROM address_types +ORDER BY id +` + +func (q *Queries) ListAddressTypes(ctx context.Context) ([]AddressType, error) { + rows, err := q.query(ctx, q.listAddressTypesStmt, ListAddressTypes) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AddressType + for rows.Next() { + var i AddressType + if err := rows.Scan(&i.ID, &i.TypeName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/wallet/internal/sql/sqlite/sqlc/addresses.sql.go b/wallet/internal/sql/sqlite/sqlc/addresses.sql.go new file mode 100644 index 0000000000..e5767ed8b8 --- /dev/null +++ b/wallet/internal/sql/sqlite/sqlc/addresses.sql.go @@ -0,0 +1,213 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: addresses.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const CreateAddress = `-- name: CreateAddress :exec +INSERT INTO addresses ( + wallet_id, + scope_id, + address_hash, + account_number, + address_type, + added_at, + sync_status, + branch, + address_index, + encrypted_pub_key, + encrypted_priv_key, + encrypted_hash, + encrypted_script, + witness_version, + is_secret_script, + used +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +` + +type CreateAddressParams struct { + WalletID int64 + ScopeID int64 + AddressHash []byte + AccountNumber int64 + AddressType int64 + AddedAt int64 + SyncStatus int64 + Branch sql.NullInt64 + AddressIndex sql.NullInt64 + EncryptedPubKey []byte + EncryptedPrivKey []byte + EncryptedHash []byte + EncryptedScript []byte + WitnessVersion sql.NullInt64 + IsSecretScript sql.NullBool + Used bool +} + +func (q *Queries) CreateAddress(ctx context.Context, arg CreateAddressParams) error { + _, err := q.exec(ctx, q.createAddressStmt, CreateAddress, + arg.WalletID, + arg.ScopeID, + arg.AddressHash, + arg.AccountNumber, + arg.AddressType, + arg.AddedAt, + arg.SyncStatus, + arg.Branch, + arg.AddressIndex, + arg.EncryptedPubKey, + arg.EncryptedPrivKey, + arg.EncryptedHash, + arg.EncryptedScript, + arg.WitnessVersion, + arg.IsSecretScript, + arg.Used, + ) + return err +} + +const GetAddress = `-- name: GetAddress :one +SELECT + wallet_id, + scope_id, + address_hash, + account_number, + address_type, + added_at, + sync_status, + branch, + address_index, + encrypted_pub_key, + encrypted_priv_key, + encrypted_hash, + encrypted_script, + witness_version, + is_secret_script, + used +FROM addresses +WHERE wallet_id = ? AND scope_id = ? AND address_hash = ? +` + +type GetAddressParams struct { + WalletID int64 + ScopeID int64 + AddressHash []byte +} + +func (q *Queries) GetAddress(ctx context.Context, arg GetAddressParams) (Address, error) { + row := q.queryRow(ctx, q.getAddressStmt, GetAddress, arg.WalletID, arg.ScopeID, arg.AddressHash) + var i Address + err := row.Scan( + &i.WalletID, + &i.ScopeID, + &i.AddressHash, + &i.AccountNumber, + &i.AddressType, + &i.AddedAt, + &i.SyncStatus, + &i.Branch, + &i.AddressIndex, + &i.EncryptedPubKey, + &i.EncryptedPrivKey, + &i.EncryptedHash, + &i.EncryptedScript, + &i.WitnessVersion, + &i.IsSecretScript, + &i.Used, + ) + return i, err +} + +const ListAccountAddresses = `-- name: ListAccountAddresses :many +SELECT + wallet_id, + scope_id, + address_hash, + account_number, + address_type, + added_at, + sync_status, + branch, + address_index, + encrypted_pub_key, + encrypted_priv_key, + encrypted_hash, + encrypted_script, + witness_version, + is_secret_script, + used +FROM addresses +WHERE scope_id = ? AND account_number = ? +ORDER BY address_hash +` + +type ListAccountAddressesParams struct { + ScopeID int64 + AccountNumber int64 +} + +func (q *Queries) ListAccountAddresses(ctx context.Context, arg ListAccountAddressesParams) ([]Address, error) { + rows, err := q.query(ctx, q.listAccountAddressesStmt, ListAccountAddresses, arg.ScopeID, arg.AccountNumber) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Address + for rows.Next() { + var i Address + if err := rows.Scan( + &i.WalletID, + &i.ScopeID, + &i.AddressHash, + &i.AccountNumber, + &i.AddressType, + &i.AddedAt, + &i.SyncStatus, + &i.Branch, + &i.AddressIndex, + &i.EncryptedPubKey, + &i.EncryptedPrivKey, + &i.EncryptedHash, + &i.EncryptedScript, + &i.WitnessVersion, + &i.IsSecretScript, + &i.Used, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const MarkAddressUsed = `-- name: MarkAddressUsed :execrows +UPDATE addresses +SET used = TRUE +WHERE wallet_id = ? AND scope_id = ? AND address_hash = ? +` + +type MarkAddressUsedParams struct { + WalletID int64 + ScopeID int64 + AddressHash []byte +} + +func (q *Queries) MarkAddressUsed(ctx context.Context, arg MarkAddressUsedParams) (int64, error) { + result, err := q.exec(ctx, q.markAddressUsedStmt, MarkAddressUsed, arg.WalletID, arg.ScopeID, arg.AddressHash) + if err != nil { + return 0, err + } + return result.RowsAffected() +} diff --git a/wallet/internal/sql/sqlite/sqlc/db.go b/wallet/internal/sql/sqlite/sqlc/db.go index 168d8395d4..3357da35b6 100644 --- a/wallet/internal/sql/sqlite/sqlc/db.go +++ b/wallet/internal/sql/sqlite/sqlc/db.go @@ -24,28 +24,118 @@ func New(db DBTX) *Queries { func Prepare(ctx context.Context, db DBTX) (*Queries, error) { q := Queries{db: db} var err error + if q.createAccountStmt, err = db.PrepareContext(ctx, CreateAccount); err != nil { + return nil, fmt.Errorf("error preparing query CreateAccount: %w", err) + } + if q.createAddressStmt, err = db.PrepareContext(ctx, CreateAddress); err != nil { + return nil, fmt.Errorf("error preparing query CreateAddress: %w", err) + } + if q.createKeyScopeStmt, err = db.PrepareContext(ctx, CreateKeyScope); err != nil { + return nil, fmt.Errorf("error preparing query CreateKeyScope: %w", err) + } + if q.createWalletStmt, err = db.PrepareContext(ctx, CreateWallet); err != nil { + return nil, fmt.Errorf("error preparing query CreateWallet: %w", err) + } if q.deleteBlockStmt, err = db.PrepareContext(ctx, DeleteBlock); err != nil { return nil, fmt.Errorf("error preparing query DeleteBlock: %w", err) } + if q.getAccountStmt, err = db.PrepareContext(ctx, GetAccount); err != nil { + return nil, fmt.Errorf("error preparing query GetAccount: %w", err) + } + if q.getAddressStmt, err = db.PrepareContext(ctx, GetAddress); err != nil { + return nil, fmt.Errorf("error preparing query GetAddress: %w", err) + } if q.getBlockByHeightStmt, err = db.PrepareContext(ctx, GetBlockByHeight); err != nil { return nil, fmt.Errorf("error preparing query GetBlockByHeight: %w", err) } if q.getBlocksInRangeStmt, err = db.PrepareContext(ctx, GetBlocksInRange); err != nil { return nil, fmt.Errorf("error preparing query GetBlocksInRange: %w", err) } + if q.getKeyScopeStmt, err = db.PrepareContext(ctx, GetKeyScope); err != nil { + return nil, fmt.Errorf("error preparing query GetKeyScope: %w", err) + } + if q.getWalletByNameStmt, err = db.PrepareContext(ctx, GetWalletByName); err != nil { + return nil, fmt.Errorf("error preparing query GetWalletByName: %w", err) + } + if q.getWalletSyncStateStmt, err = db.PrepareContext(ctx, GetWalletSyncState); err != nil { + return nil, fmt.Errorf("error preparing query GetWalletSyncState: %w", err) + } if q.insertBlockStmt, err = db.PrepareContext(ctx, InsertBlock); err != nil { return nil, fmt.Errorf("error preparing query InsertBlock: %w", err) } + if q.listAccountAddressesStmt, err = db.PrepareContext(ctx, ListAccountAddresses); err != nil { + return nil, fmt.Errorf("error preparing query ListAccountAddresses: %w", err) + } + if q.listAccountsStmt, err = db.PrepareContext(ctx, ListAccounts); err != nil { + return nil, fmt.Errorf("error preparing query ListAccounts: %w", err) + } + if q.listAddressTypesStmt, err = db.PrepareContext(ctx, ListAddressTypes); err != nil { + return nil, fmt.Errorf("error preparing query ListAddressTypes: %w", err) + } + if q.markAddressUsedStmt, err = db.PrepareContext(ctx, MarkAddressUsed); err != nil { + return nil, fmt.Errorf("error preparing query MarkAddressUsed: %w", err) + } + if q.putWalletSyncStateStmt, err = db.PrepareContext(ctx, PutWalletSyncState); err != nil { + return nil, fmt.Errorf("error preparing query PutWalletSyncState: %w", err) + } + if q.renameAccountStmt, err = db.PrepareContext(ctx, RenameAccount); err != nil { + return nil, fmt.Errorf("error preparing query RenameAccount: %w", err) + } + if q.updateAccountIndexesStmt, err = db.PrepareContext(ctx, UpdateAccountIndexes); err != nil { + return nil, fmt.Errorf("error preparing query UpdateAccountIndexes: %w", err) + } + if q.updateKeyScopeKeysStmt, err = db.PrepareContext(ctx, UpdateKeyScopeKeys); err != nil { + return nil, fmt.Errorf("error preparing query UpdateKeyScopeKeys: %w", err) + } + if q.updateLastAccountNumberStmt, err = db.PrepareContext(ctx, UpdateLastAccountNumber); err != nil { + return nil, fmt.Errorf("error preparing query UpdateLastAccountNumber: %w", err) + } + if q.updateWalletEncryptionStmt, err = db.PrepareContext(ctx, UpdateWalletEncryption); err != nil { + return nil, fmt.Errorf("error preparing query UpdateWalletEncryption: %w", err) + } + if q.updateWalletSyncStateStmt, err = db.PrepareContext(ctx, UpdateWalletSyncState); err != nil { + return nil, fmt.Errorf("error preparing query UpdateWalletSyncState: %w", err) + } return &q, nil } func (q *Queries) Close() error { var err error + if q.createAccountStmt != nil { + if cerr := q.createAccountStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createAccountStmt: %w", cerr) + } + } + if q.createAddressStmt != nil { + if cerr := q.createAddressStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createAddressStmt: %w", cerr) + } + } + if q.createKeyScopeStmt != nil { + if cerr := q.createKeyScopeStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createKeyScopeStmt: %w", cerr) + } + } + if q.createWalletStmt != nil { + if cerr := q.createWalletStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createWalletStmt: %w", cerr) + } + } if q.deleteBlockStmt != nil { if cerr := q.deleteBlockStmt.Close(); cerr != nil { err = fmt.Errorf("error closing deleteBlockStmt: %w", cerr) } } + if q.getAccountStmt != nil { + if cerr := q.getAccountStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getAccountStmt: %w", cerr) + } + } + if q.getAddressStmt != nil { + if cerr := q.getAddressStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getAddressStmt: %w", cerr) + } + } if q.getBlockByHeightStmt != nil { if cerr := q.getBlockByHeightStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getBlockByHeightStmt: %w", cerr) @@ -56,11 +146,81 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getBlocksInRangeStmt: %w", cerr) } } + if q.getKeyScopeStmt != nil { + if cerr := q.getKeyScopeStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getKeyScopeStmt: %w", cerr) + } + } + if q.getWalletByNameStmt != nil { + if cerr := q.getWalletByNameStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getWalletByNameStmt: %w", cerr) + } + } + if q.getWalletSyncStateStmt != nil { + if cerr := q.getWalletSyncStateStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getWalletSyncStateStmt: %w", cerr) + } + } if q.insertBlockStmt != nil { if cerr := q.insertBlockStmt.Close(); cerr != nil { err = fmt.Errorf("error closing insertBlockStmt: %w", cerr) } } + if q.listAccountAddressesStmt != nil { + if cerr := q.listAccountAddressesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listAccountAddressesStmt: %w", cerr) + } + } + if q.listAccountsStmt != nil { + if cerr := q.listAccountsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listAccountsStmt: %w", cerr) + } + } + if q.listAddressTypesStmt != nil { + if cerr := q.listAddressTypesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listAddressTypesStmt: %w", cerr) + } + } + if q.markAddressUsedStmt != nil { + if cerr := q.markAddressUsedStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing markAddressUsedStmt: %w", cerr) + } + } + if q.putWalletSyncStateStmt != nil { + if cerr := q.putWalletSyncStateStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing putWalletSyncStateStmt: %w", cerr) + } + } + if q.renameAccountStmt != nil { + if cerr := q.renameAccountStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing renameAccountStmt: %w", cerr) + } + } + if q.updateAccountIndexesStmt != nil { + if cerr := q.updateAccountIndexesStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateAccountIndexesStmt: %w", cerr) + } + } + if q.updateKeyScopeKeysStmt != nil { + if cerr := q.updateKeyScopeKeysStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateKeyScopeKeysStmt: %w", cerr) + } + } + if q.updateLastAccountNumberStmt != nil { + if cerr := q.updateLastAccountNumberStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateLastAccountNumberStmt: %w", cerr) + } + } + if q.updateWalletEncryptionStmt != nil { + if cerr := q.updateWalletEncryptionStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateWalletEncryptionStmt: %w", cerr) + } + } + if q.updateWalletSyncStateStmt != nil { + if cerr := q.updateWalletSyncStateStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateWalletSyncStateStmt: %w", cerr) + } + } return err } @@ -98,21 +258,61 @@ func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, ar } type Queries struct { - db DBTX - tx *sql.Tx - deleteBlockStmt *sql.Stmt - getBlockByHeightStmt *sql.Stmt - getBlocksInRangeStmt *sql.Stmt - insertBlockStmt *sql.Stmt + db DBTX + tx *sql.Tx + createAccountStmt *sql.Stmt + createAddressStmt *sql.Stmt + createKeyScopeStmt *sql.Stmt + createWalletStmt *sql.Stmt + deleteBlockStmt *sql.Stmt + getAccountStmt *sql.Stmt + getAddressStmt *sql.Stmt + getBlockByHeightStmt *sql.Stmt + getBlocksInRangeStmt *sql.Stmt + getKeyScopeStmt *sql.Stmt + getWalletByNameStmt *sql.Stmt + getWalletSyncStateStmt *sql.Stmt + insertBlockStmt *sql.Stmt + listAccountAddressesStmt *sql.Stmt + listAccountsStmt *sql.Stmt + listAddressTypesStmt *sql.Stmt + markAddressUsedStmt *sql.Stmt + putWalletSyncStateStmt *sql.Stmt + renameAccountStmt *sql.Stmt + updateAccountIndexesStmt *sql.Stmt + updateKeyScopeKeysStmt *sql.Stmt + updateLastAccountNumberStmt *sql.Stmt + updateWalletEncryptionStmt *sql.Stmt + updateWalletSyncStateStmt *sql.Stmt } func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{ - db: tx, - tx: tx, - deleteBlockStmt: q.deleteBlockStmt, - getBlockByHeightStmt: q.getBlockByHeightStmt, - getBlocksInRangeStmt: q.getBlocksInRangeStmt, - insertBlockStmt: q.insertBlockStmt, + db: tx, + tx: tx, + createAccountStmt: q.createAccountStmt, + createAddressStmt: q.createAddressStmt, + createKeyScopeStmt: q.createKeyScopeStmt, + createWalletStmt: q.createWalletStmt, + deleteBlockStmt: q.deleteBlockStmt, + getAccountStmt: q.getAccountStmt, + getAddressStmt: q.getAddressStmt, + getBlockByHeightStmt: q.getBlockByHeightStmt, + getBlocksInRangeStmt: q.getBlocksInRangeStmt, + getKeyScopeStmt: q.getKeyScopeStmt, + getWalletByNameStmt: q.getWalletByNameStmt, + getWalletSyncStateStmt: q.getWalletSyncStateStmt, + insertBlockStmt: q.insertBlockStmt, + listAccountAddressesStmt: q.listAccountAddressesStmt, + listAccountsStmt: q.listAccountsStmt, + listAddressTypesStmt: q.listAddressTypesStmt, + markAddressUsedStmt: q.markAddressUsedStmt, + putWalletSyncStateStmt: q.putWalletSyncStateStmt, + renameAccountStmt: q.renameAccountStmt, + updateAccountIndexesStmt: q.updateAccountIndexesStmt, + updateKeyScopeKeysStmt: q.updateKeyScopeKeysStmt, + updateLastAccountNumberStmt: q.updateLastAccountNumberStmt, + updateWalletEncryptionStmt: q.updateWalletEncryptionStmt, + updateWalletSyncStateStmt: q.updateWalletSyncStateStmt, } } diff --git a/wallet/internal/sql/sqlite/sqlc/key_scopes.sql.go b/wallet/internal/sql/sqlite/sqlc/key_scopes.sql.go new file mode 100644 index 0000000000..ff19ef286b --- /dev/null +++ b/wallet/internal/sql/sqlite/sqlc/key_scopes.sql.go @@ -0,0 +1,135 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: key_scopes.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const CreateKeyScope = `-- name: CreateKeyScope :one +INSERT INTO key_scopes ( + wallet_id, + purpose, + coin_type, + encrypted_coin_pub_key, + encrypted_coin_priv_key, + external_addr_type, + internal_addr_type +) VALUES (?, ?, ?, ?, ?, ?, ?) +RETURNING id +` + +type CreateKeyScopeParams struct { + WalletID int64 + Purpose int64 + CoinType int64 + EncryptedCoinPubKey []byte + EncryptedCoinPrivKey []byte + ExternalAddrType int64 + InternalAddrType int64 +} + +func (q *Queries) CreateKeyScope(ctx context.Context, arg CreateKeyScopeParams) (int64, error) { + row := q.queryRow(ctx, q.createKeyScopeStmt, CreateKeyScope, + arg.WalletID, + arg.Purpose, + arg.CoinType, + arg.EncryptedCoinPubKey, + arg.EncryptedCoinPrivKey, + arg.ExternalAddrType, + arg.InternalAddrType, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const GetKeyScope = `-- name: GetKeyScope :one +SELECT + id, + wallet_id, + purpose, + coin_type, + encrypted_coin_pub_key, + encrypted_coin_priv_key, + last_account_number, + external_addr_type, + internal_addr_type +FROM key_scopes +WHERE wallet_id = ? AND purpose = ? AND coin_type = ? +` + +type GetKeyScopeParams struct { + WalletID int64 + Purpose int64 + CoinType int64 +} + +func (q *Queries) GetKeyScope(ctx context.Context, arg GetKeyScopeParams) (KeyScope, error) { + row := q.queryRow(ctx, q.getKeyScopeStmt, GetKeyScope, arg.WalletID, arg.Purpose, arg.CoinType) + var i KeyScope + err := row.Scan( + &i.ID, + &i.WalletID, + &i.Purpose, + &i.CoinType, + &i.EncryptedCoinPubKey, + &i.EncryptedCoinPrivKey, + &i.LastAccountNumber, + &i.ExternalAddrType, + &i.InternalAddrType, + ) + return i, err +} + +const UpdateKeyScopeKeys = `-- name: UpdateKeyScopeKeys :execrows +UPDATE key_scopes +SET + encrypted_coin_pub_key = ?, + encrypted_coin_priv_key = ? +WHERE id = ? AND wallet_id = ? +` + +type UpdateKeyScopeKeysParams struct { + EncryptedCoinPubKey []byte + EncryptedCoinPrivKey []byte + ID int64 + WalletID int64 +} + +func (q *Queries) UpdateKeyScopeKeys(ctx context.Context, arg UpdateKeyScopeKeysParams) (int64, error) { + result, err := q.exec(ctx, q.updateKeyScopeKeysStmt, UpdateKeyScopeKeys, + arg.EncryptedCoinPubKey, + arg.EncryptedCoinPrivKey, + arg.ID, + arg.WalletID, + ) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const UpdateLastAccountNumber = `-- name: UpdateLastAccountNumber :execrows +UPDATE key_scopes +SET last_account_number = ? +WHERE id = ? AND wallet_id = ? +` + +type UpdateLastAccountNumberParams struct { + LastAccountNumber sql.NullInt64 + ID int64 + WalletID int64 +} + +func (q *Queries) UpdateLastAccountNumber(ctx context.Context, arg UpdateLastAccountNumberParams) (int64, error) { + result, err := q.exec(ctx, q.updateLastAccountNumberStmt, UpdateLastAccountNumber, arg.LastAccountNumber, arg.ID, arg.WalletID) + if err != nil { + return 0, err + } + return result.RowsAffected() +} diff --git a/wallet/internal/sql/sqlite/sqlc/models.go b/wallet/internal/sql/sqlite/sqlc/models.go index d99a13623f..ff251e8271 100644 --- a/wallet/internal/sql/sqlite/sqlc/models.go +++ b/wallet/internal/sql/sqlite/sqlc/models.go @@ -4,8 +4,87 @@ package sqlc +import ( + "database/sql" +) + +type Account struct { + WalletID int64 + ScopeID int64 + AccountNumber int64 + AccountType int64 + AccountName string + EncryptedPubKey []byte + EncryptedPrivKey []byte + MasterKeyFingerprint sql.NullInt64 + NextExternalIndex int64 + NextInternalIndex int64 + ExternalAddrType sql.NullInt64 + InternalAddrType sql.NullInt64 +} + +type Address struct { + WalletID int64 + ScopeID int64 + AddressHash []byte + AccountNumber int64 + AddressType int64 + AddedAt int64 + SyncStatus int64 + Branch sql.NullInt64 + AddressIndex sql.NullInt64 + EncryptedPubKey []byte + EncryptedPrivKey []byte + EncryptedHash []byte + EncryptedScript []byte + WitnessVersion sql.NullInt64 + IsSecretScript sql.NullBool + Used bool +} + +type AddressType struct { + ID int64 + TypeName string +} + type Block struct { BlockHeight int64 HeaderHash []byte BlockTimestamp int64 } + +type KeyScope struct { + ID int64 + WalletID int64 + Purpose int64 + CoinType int64 + EncryptedCoinPubKey []byte + EncryptedCoinPrivKey []byte + LastAccountNumber sql.NullInt64 + ExternalAddrType int64 + InternalAddrType int64 +} + +type Wallet struct { + ID int64 + WalletName string + ManagerVersion int64 + ManagerCreatedAt int64 + IsWatchOnly bool + MasterPubParams []byte + MasterPrivParams []byte + EncryptedCryptoPubKey []byte + EncryptedCryptoPrivKey []byte + EncryptedCryptoScriptKey []byte + EncryptedMasterHdPubKey []byte + EncryptedMasterHdPrivKey []byte +} + +type WalletSyncState struct { + WalletID int64 + StartBlockHeight int64 + SyncedBlockHeight int64 + BirthdayTimestamp int64 + BirthdayBlockHeight sql.NullInt64 + BirthdayBlockVerified bool +} diff --git a/wallet/internal/sql/sqlite/sqlc/querier.go b/wallet/internal/sql/sqlite/sqlc/querier.go index 815602423a..a47e92882b 100644 --- a/wallet/internal/sql/sqlite/sqlc/querier.go +++ b/wallet/internal/sql/sqlite/sqlc/querier.go @@ -9,10 +9,30 @@ import ( ) type Querier interface { + CreateAccount(ctx context.Context, arg CreateAccountParams) error + CreateAddress(ctx context.Context, arg CreateAddressParams) error + CreateKeyScope(ctx context.Context, arg CreateKeyScopeParams) (int64, error) + CreateWallet(ctx context.Context, arg CreateWalletParams) (int64, error) DeleteBlock(ctx context.Context, blockHeight int64) error + GetAccount(ctx context.Context, arg GetAccountParams) (Account, error) + GetAddress(ctx context.Context, arg GetAddressParams) (Address, error) GetBlockByHeight(ctx context.Context, blockHeight int64) (Block, error) GetBlocksInRange(ctx context.Context, arg GetBlocksInRangeParams) ([]Block, error) + GetKeyScope(ctx context.Context, arg GetKeyScopeParams) (KeyScope, error) + GetWalletByName(ctx context.Context, walletName string) (Wallet, error) + GetWalletSyncState(ctx context.Context, walletID int64) (GetWalletSyncStateRow, error) InsertBlock(ctx context.Context, arg InsertBlockParams) error + ListAccountAddresses(ctx context.Context, arg ListAccountAddressesParams) ([]Address, error) + ListAccounts(ctx context.Context, scopeID int64) ([]Account, error) + ListAddressTypes(ctx context.Context) ([]AddressType, error) + MarkAddressUsed(ctx context.Context, arg MarkAddressUsedParams) (int64, error) + PutWalletSyncState(ctx context.Context, arg PutWalletSyncStateParams) error + RenameAccount(ctx context.Context, arg RenameAccountParams) (int64, error) + UpdateAccountIndexes(ctx context.Context, arg UpdateAccountIndexesParams) (int64, error) + UpdateKeyScopeKeys(ctx context.Context, arg UpdateKeyScopeKeysParams) (int64, error) + UpdateLastAccountNumber(ctx context.Context, arg UpdateLastAccountNumberParams) (int64, error) + UpdateWalletEncryption(ctx context.Context, arg UpdateWalletEncryptionParams) (int64, error) + UpdateWalletSyncState(ctx context.Context, arg UpdateWalletSyncStateParams) (int64, error) } var _ Querier = (*Queries)(nil) diff --git a/wallet/internal/sql/sqlite/sqlc/wallets.sql.go b/wallet/internal/sql/sqlite/sqlc/wallets.sql.go new file mode 100644 index 0000000000..b99ef58f75 --- /dev/null +++ b/wallet/internal/sql/sqlite/sqlc/wallets.sql.go @@ -0,0 +1,257 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: wallets.sql + +package sqlc + +import ( + "context" + "database/sql" +) + +const CreateWallet = `-- name: CreateWallet :one +INSERT INTO wallets ( + wallet_name, + manager_version, + manager_created_at, + is_watch_only, + master_pub_params, + master_priv_params, + encrypted_crypto_pub_key, + encrypted_crypto_priv_key, + encrypted_crypto_script_key, + encrypted_master_hd_pub_key, + encrypted_master_hd_priv_key +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +RETURNING id +` + +type CreateWalletParams struct { + WalletName string + ManagerVersion int64 + ManagerCreatedAt int64 + IsWatchOnly bool + MasterPubParams []byte + MasterPrivParams []byte + EncryptedCryptoPubKey []byte + EncryptedCryptoPrivKey []byte + EncryptedCryptoScriptKey []byte + EncryptedMasterHdPubKey []byte + EncryptedMasterHdPrivKey []byte +} + +func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (int64, error) { + row := q.queryRow(ctx, q.createWalletStmt, CreateWallet, + arg.WalletName, + arg.ManagerVersion, + arg.ManagerCreatedAt, + arg.IsWatchOnly, + arg.MasterPubParams, + arg.MasterPrivParams, + arg.EncryptedCryptoPubKey, + arg.EncryptedCryptoPrivKey, + arg.EncryptedCryptoScriptKey, + arg.EncryptedMasterHdPubKey, + arg.EncryptedMasterHdPrivKey, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + +const GetWalletByName = `-- name: GetWalletByName :one +SELECT + id, + wallet_name, + manager_version, + manager_created_at, + is_watch_only, + master_pub_params, + master_priv_params, + encrypted_crypto_pub_key, + encrypted_crypto_priv_key, + encrypted_crypto_script_key, + encrypted_master_hd_pub_key, + encrypted_master_hd_priv_key +FROM wallets +WHERE wallet_name = ? +` + +func (q *Queries) GetWalletByName(ctx context.Context, walletName string) (Wallet, error) { + row := q.queryRow(ctx, q.getWalletByNameStmt, GetWalletByName, walletName) + var i Wallet + err := row.Scan( + &i.ID, + &i.WalletName, + &i.ManagerVersion, + &i.ManagerCreatedAt, + &i.IsWatchOnly, + &i.MasterPubParams, + &i.MasterPrivParams, + &i.EncryptedCryptoPubKey, + &i.EncryptedCryptoPrivKey, + &i.EncryptedCryptoScriptKey, + &i.EncryptedMasterHdPubKey, + &i.EncryptedMasterHdPrivKey, + ) + return i, err +} + +const GetWalletSyncState = `-- name: GetWalletSyncState :one +SELECT + s.wallet_id, + s.start_block_height, + start_block.header_hash AS start_block_hash, + s.synced_block_height, + synced_block.header_hash AS synced_block_hash, + s.birthday_timestamp, + s.birthday_block_height, + birthday_block.header_hash AS birthday_block_hash, + s.birthday_block_verified +FROM wallet_sync_states AS s +INNER JOIN blocks AS start_block + ON s.start_block_height = start_block.block_height +INNER JOIN blocks AS synced_block + ON s.synced_block_height = synced_block.block_height +LEFT JOIN blocks AS birthday_block + ON s.birthday_block_height = birthday_block.block_height +WHERE s.wallet_id = ? +` + +type GetWalletSyncStateRow struct { + WalletID int64 + StartBlockHeight int64 + StartBlockHash []byte + SyncedBlockHeight int64 + SyncedBlockHash []byte + BirthdayTimestamp int64 + BirthdayBlockHeight sql.NullInt64 + BirthdayBlockHash []byte + BirthdayBlockVerified bool +} + +func (q *Queries) GetWalletSyncState(ctx context.Context, walletID int64) (GetWalletSyncStateRow, error) { + row := q.queryRow(ctx, q.getWalletSyncStateStmt, GetWalletSyncState, walletID) + var i GetWalletSyncStateRow + err := row.Scan( + &i.WalletID, + &i.StartBlockHeight, + &i.StartBlockHash, + &i.SyncedBlockHeight, + &i.SyncedBlockHash, + &i.BirthdayTimestamp, + &i.BirthdayBlockHeight, + &i.BirthdayBlockHash, + &i.BirthdayBlockVerified, + ) + return i, err +} + +const PutWalletSyncState = `-- name: PutWalletSyncState :exec +INSERT INTO wallet_sync_states ( + wallet_id, + start_block_height, + synced_block_height, + birthday_timestamp, + birthday_block_height, + birthday_block_verified +) VALUES (?, ?, ?, ?, ?, ?) +` + +type PutWalletSyncStateParams struct { + WalletID int64 + StartBlockHeight int64 + SyncedBlockHeight int64 + BirthdayTimestamp int64 + BirthdayBlockHeight sql.NullInt64 + BirthdayBlockVerified bool +} + +func (q *Queries) PutWalletSyncState(ctx context.Context, arg PutWalletSyncStateParams) error { + _, err := q.exec(ctx, q.putWalletSyncStateStmt, PutWalletSyncState, + arg.WalletID, + arg.StartBlockHeight, + arg.SyncedBlockHeight, + arg.BirthdayTimestamp, + arg.BirthdayBlockHeight, + arg.BirthdayBlockVerified, + ) + return err +} + +const UpdateWalletEncryption = `-- name: UpdateWalletEncryption :execrows +UPDATE wallets +SET + master_pub_params = ?, + master_priv_params = ?, + encrypted_crypto_pub_key = ?, + encrypted_crypto_priv_key = ?, + encrypted_crypto_script_key = ?, + encrypted_master_hd_pub_key = ?, + encrypted_master_hd_priv_key = ? +WHERE id = ? +` + +type UpdateWalletEncryptionParams struct { + MasterPubParams []byte + MasterPrivParams []byte + EncryptedCryptoPubKey []byte + EncryptedCryptoPrivKey []byte + EncryptedCryptoScriptKey []byte + EncryptedMasterHdPubKey []byte + EncryptedMasterHdPrivKey []byte + ID int64 +} + +func (q *Queries) UpdateWalletEncryption(ctx context.Context, arg UpdateWalletEncryptionParams) (int64, error) { + result, err := q.exec(ctx, q.updateWalletEncryptionStmt, UpdateWalletEncryption, + arg.MasterPubParams, + arg.MasterPrivParams, + arg.EncryptedCryptoPubKey, + arg.EncryptedCryptoPrivKey, + arg.EncryptedCryptoScriptKey, + arg.EncryptedMasterHdPubKey, + arg.EncryptedMasterHdPrivKey, + arg.ID, + ) + if err != nil { + return 0, err + } + return result.RowsAffected() +} + +const UpdateWalletSyncState = `-- name: UpdateWalletSyncState :execrows +UPDATE wallet_sync_states +SET + start_block_height = ?, + synced_block_height = ?, + birthday_timestamp = ?, + birthday_block_height = ?, + birthday_block_verified = ? +WHERE wallet_id = ? +` + +type UpdateWalletSyncStateParams struct { + StartBlockHeight int64 + SyncedBlockHeight int64 + BirthdayTimestamp int64 + BirthdayBlockHeight sql.NullInt64 + BirthdayBlockVerified bool + WalletID int64 +} + +func (q *Queries) UpdateWalletSyncState(ctx context.Context, arg UpdateWalletSyncStateParams) (int64, error) { + result, err := q.exec(ctx, q.updateWalletSyncStateStmt, UpdateWalletSyncState, + arg.StartBlockHeight, + arg.SyncedBlockHeight, + arg.BirthdayTimestamp, + arg.BirthdayBlockHeight, + arg.BirthdayBlockVerified, + arg.WalletID, + ) + if err != nil { + return 0, err + } + return result.RowsAffected() +}