Craig Raw
4 years ago
52 changed files with 1987 additions and 281 deletions
@ -1 +1 @@ |
|||
Subproject commit 42ffeb95650c56bffbd5ec8f8e8f38d91faaab3f |
|||
Subproject commit 8e3d0d23c129b7fe9eedb16769827155f53c84d5 |
@ -0,0 +1,22 @@ |
|||
package com.sparrowwallet.sparrow.event; |
|||
|
|||
import com.sparrowwallet.drongo.wallet.Keystore; |
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* This event is trigger when one or more keystores on a wallet are updated, and the wallet is saved |
|||
*/ |
|||
public class KeystoreLabelsChangedEvent extends WalletSettingsChangedEvent { |
|||
private final List<Keystore> changedKeystores; |
|||
|
|||
public KeystoreLabelsChangedEvent(Wallet wallet, Wallet pastWallet, String walletId, List<Keystore> changedKeystores) { |
|||
super(wallet, pastWallet, walletId); |
|||
this.changedKeystores = changedKeystores; |
|||
} |
|||
|
|||
public List<Keystore> getChangedKeystores() { |
|||
return changedKeystores; |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
package com.sparrowwallet.sparrow.event; |
|||
|
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
|
|||
public class WalletHistoryClearedEvent extends WalletSettingsChangedEvent { |
|||
public WalletHistoryClearedEvent(Wallet wallet, Wallet pastWallet, String walletId) { |
|||
super(wallet, pastWallet, walletId); |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
package com.sparrowwallet.sparrow.event; |
|||
|
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
|
|||
public class WalletPasswordChangedEvent extends WalletSettingsChangedEvent { |
|||
public WalletPasswordChangedEvent(Wallet wallet, Wallet pastWallet, String walletId) { |
|||
super(wallet, pastWallet, walletId); |
|||
} |
|||
} |
@ -0,0 +1,60 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.sparrowwallet.drongo.protocol.Sha256Hash; |
|||
import com.sparrowwallet.drongo.wallet.BlockTransaction; |
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper; |
|||
import org.jdbi.v3.sqlobject.customizer.Bind; |
|||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; |
|||
import org.jdbi.v3.sqlobject.statement.SqlQuery; |
|||
import org.jdbi.v3.sqlobject.statement.SqlUpdate; |
|||
|
|||
import java.util.Date; |
|||
import java.util.Map; |
|||
|
|||
public interface BlockTransactionDao { |
|||
@SqlQuery("select id, txid, hash, height, date, fee, label, transaction, blockHash from blockTransaction where wallet = ? order by id") |
|||
@RegisterRowMapper(BlockTransactionMapper.class) |
|||
Map<Sha256Hash, BlockTransaction> getForWalletId(Long id); |
|||
|
|||
@SqlQuery("select id, txid, hash, height, date, fee, label, transaction, blockHash from blockTransaction where txid = ?") |
|||
@RegisterRowMapper(BlockTransactionMapper.class) |
|||
Map<Sha256Hash, BlockTransaction> getForTxId(byte[] id); |
|||
|
|||
@SqlUpdate("insert into blockTransaction (txid, hash, height, date, fee, label, transaction, blockHash, wallet) values (?, ?, ?, ?, ?, ?, ?, ?, ?)") |
|||
@GetGeneratedKeys("id") |
|||
long insertBlockTransaction(byte[] txid, byte[] hash, int height, Date date, Long fee, String label, byte[] transaction, byte[] blockHash, long wallet); |
|||
|
|||
@SqlUpdate("update blockTransaction set txid = ?, hash = ?, height = ?, date = ?, fee = ?, label = ?, transaction = ?, blockHash = ?, wallet = ? where id = ?") |
|||
void updateBlockTransaction(byte[] txid, byte[] hash, int height, Date date, Long fee, String label, byte[] transaction, byte[] blockHash, long wallet, long id); |
|||
|
|||
@SqlUpdate("update blockTransaction set label = :label where id = :id") |
|||
void updateLabel(@Bind("id") long id, @Bind("label") String label); |
|||
|
|||
@SqlUpdate("delete from blockTransaction where wallet = ?") |
|||
void clear(long wallet); |
|||
|
|||
default void addBlockTransactions(Wallet wallet) { |
|||
for(Map.Entry<Sha256Hash, BlockTransaction> blkTxEntry : wallet.getTransactions().entrySet()) { |
|||
blkTxEntry.getValue().setId(null); |
|||
addOrUpdate(wallet, blkTxEntry.getKey(), blkTxEntry.getValue()); |
|||
} |
|||
} |
|||
|
|||
default void addOrUpdate(Wallet wallet, Sha256Hash txid, BlockTransaction blkTx) { |
|||
Map<Sha256Hash, BlockTransaction> existing = getForTxId(txid.getBytes()); |
|||
|
|||
if(existing.isEmpty() && blkTx.getId() == null) { |
|||
long id = insertBlockTransaction(txid.getBytes(), blkTx.getHash().getBytes(), blkTx.getHeight(), blkTx.getDate(), blkTx.getFee(), blkTx.getLabel(), |
|||
blkTx.getTransaction() == null ? null : blkTx.getTransaction().bitcoinSerialize(), |
|||
blkTx.getBlockHash() == null ? null : blkTx.getBlockHash().getBytes(), wallet.getId()); |
|||
blkTx.setId(id); |
|||
} else { |
|||
Long existingId = existing.get(txid) != null ? existing.get(txid).getId() : blkTx.getId(); |
|||
updateBlockTransaction(txid.getBytes(), blkTx.getHash().getBytes(), blkTx.getHeight(), blkTx.getDate(), blkTx.getFee(), blkTx.getLabel(), |
|||
blkTx.getTransaction() == null ? null : blkTx.getTransaction().bitcoinSerialize(), |
|||
blkTx.getBlockHash() == null ? null : blkTx.getBlockHash().getBytes(), wallet.getId(), existingId); |
|||
blkTx.setId(existingId); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,26 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.sparrowwallet.drongo.protocol.Sha256Hash; |
|||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; |
|||
import com.sparrowwallet.drongo.wallet.Status; |
|||
import org.jdbi.v3.core.mapper.RowMapper; |
|||
import org.jdbi.v3.core.statement.StatementContext; |
|||
|
|||
import java.sql.ResultSet; |
|||
import java.sql.SQLException; |
|||
|
|||
public class BlockTransactionHashIndexMapper implements RowMapper<BlockTransactionHashIndex> { |
|||
@Override |
|||
public BlockTransactionHashIndex map(ResultSet rs, StatementContext ctx) throws SQLException { |
|||
BlockTransactionHashIndex blockTransactionHashIndex = new BlockTransactionHashIndex(Sha256Hash.wrap(rs.getBytes("blockTransactionHashIndex.hash")), |
|||
rs.getInt("blockTransactionHashIndex.height"), rs.getTimestamp("blockTransactionHashIndex.date"), rs.getLong("blockTransactionHashIndex.fee"), |
|||
rs.getLong("blockTransactionHashIndex.index"), rs.getLong("blockTransactionHashIndex.value"), null, rs.getString("blockTransactionHashIndex.label")); |
|||
blockTransactionHashIndex.setId(rs.getLong("blockTransactionHashIndex.id")); |
|||
int statusIndex = rs.getInt("blockTransactionHashIndex.status"); |
|||
if(!rs.wasNull()) { |
|||
blockTransactionHashIndex.setStatus(Status.values()[statusIndex]); |
|||
} |
|||
|
|||
return blockTransactionHashIndex; |
|||
} |
|||
} |
@ -0,0 +1,51 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.sparrowwallet.drongo.protocol.Sha256Hash; |
|||
import com.sparrowwallet.drongo.protocol.Transaction; |
|||
import com.sparrowwallet.drongo.wallet.BlockTransaction; |
|||
import org.jdbi.v3.core.mapper.RowMapper; |
|||
import org.jdbi.v3.core.statement.StatementContext; |
|||
|
|||
import java.sql.ResultSet; |
|||
import java.sql.SQLException; |
|||
import java.util.Map; |
|||
|
|||
public class BlockTransactionMapper implements RowMapper<Map.Entry<Sha256Hash, BlockTransaction>> { |
|||
|
|||
@Override |
|||
public Map.Entry<Sha256Hash, BlockTransaction> map(ResultSet rs, StatementContext ctx) throws SQLException { |
|||
Sha256Hash txid = Sha256Hash.wrap(rs.getBytes("txid")); |
|||
|
|||
byte[] txBytes = rs.getBytes("transaction"); |
|||
Transaction transaction = null; |
|||
if(txBytes != null) { |
|||
transaction = new Transaction(txBytes); |
|||
} |
|||
|
|||
Long fee = rs.getLong("fee"); |
|||
if(rs.wasNull()) { |
|||
fee = null; |
|||
} |
|||
|
|||
BlockTransaction blockTransaction = new BlockTransaction(Sha256Hash.wrap(rs.getBytes("hash")), rs.getInt("height"), rs.getTimestamp("date"), |
|||
fee, transaction, rs.getBytes("blockHash") == null ? null : Sha256Hash.wrap(rs.getBytes("blockHash")), rs.getString("label")); |
|||
blockTransaction.setId(rs.getLong("id")); |
|||
|
|||
return new Map.Entry<>() { |
|||
@Override |
|||
public Sha256Hash getKey() { |
|||
return txid; |
|||
} |
|||
|
|||
@Override |
|||
public BlockTransaction getValue() { |
|||
return blockTransaction; |
|||
} |
|||
|
|||
@Override |
|||
public BlockTransaction setValue(BlockTransaction value) { |
|||
return null; |
|||
} |
|||
}; |
|||
} |
|||
} |
@ -0,0 +1,607 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.google.common.eventbus.Subscribe; |
|||
import com.sparrowwallet.drongo.Utils; |
|||
import com.sparrowwallet.drongo.crypto.Argon2KeyDeriver; |
|||
import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver; |
|||
import com.sparrowwallet.drongo.crypto.ECKey; |
|||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException; |
|||
import com.sparrowwallet.drongo.protocol.Sha256Hash; |
|||
import com.sparrowwallet.drongo.wallet.*; |
|||
import com.sparrowwallet.sparrow.EventManager; |
|||
import com.sparrowwallet.sparrow.event.*; |
|||
import com.sparrowwallet.sparrow.io.*; |
|||
import com.sparrowwallet.sparrow.wallet.*; |
|||
import com.zaxxer.hikari.HikariConfig; |
|||
import com.zaxxer.hikari.HikariDataSource; |
|||
import com.zaxxer.hikari.pool.HikariPool; |
|||
import org.flywaydb.core.Flyway; |
|||
import org.flywaydb.core.api.FlywayException; |
|||
import org.flywaydb.core.api.exception.FlywayValidateException; |
|||
import org.h2.tools.ChangeFileEncryption; |
|||
import org.jdbi.v3.core.Jdbi; |
|||
import org.jdbi.v3.core.h2.H2DatabasePlugin; |
|||
import org.jdbi.v3.sqlobject.SqlObjectPlugin; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
|
|||
import java.io.*; |
|||
import java.nio.ByteBuffer; |
|||
import java.nio.channels.FileChannel; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.security.SecureRandom; |
|||
import java.util.*; |
|||
import java.util.stream.Collectors; |
|||
import java.util.stream.Stream; |
|||
|
|||
public class DbPersistence implements Persistence { |
|||
private static final Logger log = LoggerFactory.getLogger(DbPersistence.class); |
|||
|
|||
static final String DEFAULT_SCHEMA = "PUBLIC"; |
|||
private static final String WALLET_SCHEMA_PREFIX = "wallet_"; |
|||
private static final String MASTER_SCHEMA = WALLET_SCHEMA_PREFIX + "master"; |
|||
private static final byte[] H2_ENCRYPT_HEADER = "H2encrypt\n".getBytes(StandardCharsets.UTF_8); |
|||
private static final int H2_ENCRYPT_SALT_LENGTH_BYTES = 8; |
|||
private static final int SALT_LENGTH_BYTES = 16; |
|||
public static final byte[] HEADER_MAGIC_1 = "SPRW1\n".getBytes(StandardCharsets.UTF_8); |
|||
private static final String H2_USER = "sa"; |
|||
private static final String H2_PASSWORD = ""; |
|||
|
|||
private HikariDataSource dataSource; |
|||
private AsymmetricKeyDeriver keyDeriver; |
|||
|
|||
private Wallet masterWallet; |
|||
private final Map<Wallet, DirtyPersistables> dirtyPersistablesMap = new HashMap<>(); |
|||
|
|||
public DbPersistence() { |
|||
EventManager.get().register(this); |
|||
} |
|||
|
|||
@Override |
|||
public WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException { |
|||
return loadWallet(storage, null, null); |
|||
} |
|||
|
|||
@Override |
|||
public WalletBackupAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException { |
|||
return loadWallet(storage, password, null); |
|||
} |
|||
|
|||
@Override |
|||
public WalletBackupAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException { |
|||
ECKey encryptionKey = getEncryptionKey(password, storage.getWalletFile(), alreadyDerivedKey); |
|||
|
|||
migrate(storage, MASTER_SCHEMA, encryptionKey); |
|||
|
|||
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey)); |
|||
masterWallet = jdbi.withHandle(handle -> { |
|||
WalletDao walletDao = handle.attach(WalletDao.class); |
|||
return walletDao.getMainWallet(MASTER_SCHEMA); |
|||
}); |
|||
|
|||
File backupFile = storage.getTempBackup(); |
|||
Wallet backupWallet = null; |
|||
if(backupFile != null) { |
|||
Persistence backupPersistence = PersistenceType.DB.getInstance(); |
|||
backupWallet = backupPersistence.loadWallet(new Storage(backupPersistence, backupFile), password, encryptionKey).getWallet(); |
|||
} |
|||
|
|||
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, masterWallet, backupWallet, encryptionKey); |
|||
masterWallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList())); |
|||
|
|||
return new WalletBackupAndKey(masterWallet, backupWallet, encryptionKey, keyDeriver, childWallets); |
|||
} |
|||
|
|||
private Map<Storage, WalletBackupAndKey> loadChildWallets(Storage storage, Wallet masterWallet, Wallet backupWallet, ECKey encryptionKey) throws StorageException { |
|||
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey)); |
|||
List<String> schemas = jdbi.withHandle(handle -> { |
|||
return handle.createQuery("show schemas").mapTo(String.class).list(); |
|||
}); |
|||
|
|||
List<String> childSchemas = schemas.stream().filter(schema -> schema.startsWith(WALLET_SCHEMA_PREFIX) && !schema.equals(MASTER_SCHEMA)).collect(Collectors.toList()); |
|||
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>(); |
|||
for(String schema : childSchemas) { |
|||
migrate(storage, schema, encryptionKey); |
|||
|
|||
Jdbi childJdbi = getJdbi(storage, getFilePassword(encryptionKey)); |
|||
Wallet wallet = childJdbi.withHandle(handle -> { |
|||
WalletDao walletDao = handle.attach(WalletDao.class); |
|||
Wallet childWallet = walletDao.getMainWallet(schema); |
|||
childWallet.setName(schema.substring(WALLET_SCHEMA_PREFIX.length())); |
|||
childWallet.setMasterWallet(masterWallet); |
|||
return childWallet; |
|||
}); |
|||
Wallet backupChildWallet = backupWallet == null ? null : backupWallet.getChildWallets().stream().filter(child -> wallet.getName().equals(child.getName())).findFirst().orElse(null); |
|||
childWallets.put(storage, new WalletBackupAndKey(wallet, backupChildWallet, encryptionKey, keyDeriver, Collections.emptyMap())); |
|||
} |
|||
|
|||
return childWallets; |
|||
} |
|||
|
|||
@Override |
|||
public File storeWallet(Storage storage, Wallet wallet) throws IOException, StorageException { |
|||
File walletFile = storage.getWalletFile(); |
|||
walletFile = renameToDbFile(walletFile); |
|||
|
|||
if(walletFile.exists() && isEncrypted(walletFile)) { |
|||
if(dataSource != null && !dataSource.isClosed()) { |
|||
dataSource.close(); |
|||
} |
|||
walletFile.delete(); |
|||
} |
|||
|
|||
updatePassword(storage, null); |
|||
cleanAndAddWallet(storage, wallet, null); |
|||
|
|||
return walletFile; |
|||
} |
|||
|
|||
@Override |
|||
public File storeWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException, StorageException { |
|||
File walletFile = storage.getWalletFile(); |
|||
walletFile = renameToDbFile(walletFile); |
|||
|
|||
if(walletFile.exists() && !isEncrypted(walletFile)) { |
|||
if(dataSource != null && !dataSource.isClosed()) { |
|||
dataSource.close(); |
|||
} |
|||
walletFile.delete(); |
|||
} |
|||
|
|||
boolean existing = walletFile.exists(); |
|||
updatePassword(storage, encryptionPubKey); |
|||
cleanAndAddWallet(storage, wallet, getFilePassword(encryptionPubKey)); |
|||
if(!existing) { |
|||
writeBinaryHeader(walletFile); |
|||
} |
|||
|
|||
return walletFile; |
|||
} |
|||
|
|||
@Override |
|||
public void updateWallet(Storage storage, Wallet wallet) throws StorageException { |
|||
updateWallet(storage, wallet, null); |
|||
} |
|||
|
|||
@Override |
|||
public void updateWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws StorageException { |
|||
updatePassword(storage, encryptionPubKey); |
|||
update(storage, wallet, getFilePassword(encryptionPubKey)); |
|||
} |
|||
|
|||
private File renameToDbFile(File walletFile) throws IOException { |
|||
if(!walletFile.getName().endsWith("." + getType().getExtension())) { |
|||
File dbFile = new File(walletFile.getParentFile(), walletFile.getName() + "." + getType().getExtension()); |
|||
if(walletFile.exists()) { |
|||
if(!walletFile.renameTo(dbFile)) { |
|||
throw new IOException("Could not rename " + walletFile.getName() + " to " + dbFile.getName()); |
|||
} |
|||
} |
|||
|
|||
return dbFile; |
|||
} |
|||
|
|||
return walletFile; |
|||
} |
|||
|
|||
private void update(Storage storage, Wallet wallet, String password) throws StorageException { |
|||
DirtyPersistables dirtyPersistables = dirtyPersistablesMap.get(wallet); |
|||
if(dirtyPersistables == null) { |
|||
return; |
|||
} |
|||
|
|||
log.debug("Updating " + wallet.getName() + " on " + Thread.currentThread().getName()); |
|||
log.debug(dirtyPersistables.toString()); |
|||
|
|||
Jdbi jdbi = getJdbi(storage, password); |
|||
jdbi.useHandle(handle -> { |
|||
WalletDao walletDao = handle.attach(WalletDao.class); |
|||
try { |
|||
walletDao.setSchema(getSchema(wallet)); |
|||
|
|||
if(dirtyPersistables.clearHistory) { |
|||
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class); |
|||
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class); |
|||
walletNodeDao.clearHistory(wallet); |
|||
blockTransactionDao.clear(wallet.getId()); |
|||
} |
|||
|
|||
if(!dirtyPersistables.historyNodes.isEmpty()) { |
|||
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class); |
|||
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class); |
|||
Set<Sha256Hash> referencedTxIds = new HashSet<>(); |
|||
for(WalletNode addressNode : dirtyPersistables.historyNodes) { |
|||
if(addressNode.getId() == null) { |
|||
WalletNode purposeNode = wallet.getNode(addressNode.getKeyPurpose()); |
|||
if(purposeNode.getId() == null) { |
|||
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null); |
|||
purposeNode.setId(purposeNodeId); |
|||
} |
|||
|
|||
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId()); |
|||
addressNode.setId(nodeId); |
|||
} |
|||
|
|||
Set<BlockTransactionHashIndex> txos = addressNode.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo)).collect(Collectors.toSet()); |
|||
List<Long> existingIds = txos.stream().map(Persistable::getId).filter(Objects::nonNull).collect(Collectors.toList()); |
|||
referencedTxIds.addAll(txos.stream().map(BlockTransactionHash::getHash).collect(Collectors.toSet())); |
|||
|
|||
walletNodeDao.deleteNodeTxosNotInList(addressNode, existingIds.isEmpty() ? List.of(-1L) : existingIds); |
|||
for(BlockTransactionHashIndex txo : addressNode.getTransactionOutputs()) { |
|||
walletNodeDao.addOrUpdate(addressNode, txo); |
|||
} |
|||
} |
|||
for(Sha256Hash txid : referencedTxIds) { |
|||
BlockTransaction blkTx = wallet.getTransactions().get(txid); |
|||
blockTransactionDao.addOrUpdate(wallet, txid, blkTx); |
|||
} |
|||
} |
|||
|
|||
if(dirtyPersistables.blockHeight != null) { |
|||
walletDao.updateStoredBlockHeight(wallet.getId(), dirtyPersistables.blockHeight); |
|||
} |
|||
|
|||
if(!dirtyPersistables.labelEntries.isEmpty()) { |
|||
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class); |
|||
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class); |
|||
for(Entry entry : dirtyPersistables.labelEntries) { |
|||
if(entry instanceof TransactionEntry && ((TransactionEntry)entry).getBlockTransaction().getId() != null) { |
|||
blockTransactionDao.updateLabel(((TransactionEntry)entry).getBlockTransaction().getId(), entry.getLabel()); |
|||
} else if(entry instanceof NodeEntry && ((NodeEntry)entry).getNode().getId() != null) { |
|||
walletNodeDao.updateNodeLabel(((NodeEntry)entry).getNode().getId(), entry.getLabel()); |
|||
} else if(entry instanceof HashIndexEntry && ((HashIndexEntry)entry).getHashIndex().getId() != null) { |
|||
walletNodeDao.updateTxoLabel(((HashIndexEntry)entry).getHashIndex().getId(), entry.getLabel()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if(!dirtyPersistables.utxoStatuses.isEmpty()) { |
|||
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class); |
|||
for(BlockTransactionHashIndex utxo : dirtyPersistables.utxoStatuses) { |
|||
walletNodeDao.updateTxoStatus(utxo.getId(), utxo.getStatus() == null ? null : utxo.getStatus().ordinal()); |
|||
} |
|||
} |
|||
|
|||
if(!dirtyPersistables.labelKeystores.isEmpty()) { |
|||
KeystoreDao keystoreDao = handle.attach(KeystoreDao.class); |
|||
for(Keystore keystore : dirtyPersistables.labelKeystores) { |
|||
keystoreDao.updateLabel(keystore.getLabel(), keystore.getId()); |
|||
} |
|||
} |
|||
|
|||
dirtyPersistablesMap.remove(wallet); |
|||
} finally { |
|||
walletDao.setSchema(DEFAULT_SCHEMA); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private void cleanAndAddWallet(Storage storage, Wallet wallet, String password) throws StorageException { |
|||
String schema = getSchema(wallet); |
|||
cleanAndMigrate(storage, schema, password); |
|||
|
|||
Jdbi jdbi = getJdbi(storage, password); |
|||
jdbi.useHandle(handle -> { |
|||
WalletDao walletDao = handle.attach(WalletDao.class); |
|||
walletDao.addWallet(schema, wallet); |
|||
}); |
|||
|
|||
if(wallet.isMasterWallet()) { |
|||
masterWallet = wallet; |
|||
} |
|||
} |
|||
|
|||
private void migrate(Storage storage, String schema, ECKey encryptionKey) throws StorageException { |
|||
try { |
|||
Flyway flyway = getFlyway(storage, schema, getFilePassword(encryptionKey)); |
|||
flyway.migrate(); |
|||
} catch(FlywayValidateException e) { |
|||
log.error("Failed to open wallet file. Validation error during schema migration.", e); |
|||
throw new StorageException("Failed to open wallet file. Validation error during schema migration.", e); |
|||
} catch(FlywayException e) { |
|||
log.error("Failed to open wallet file. ", e); |
|||
throw new StorageException("Failed to open wallet file.\n" + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
private void cleanAndMigrate(Storage storage, String schema, String password) throws StorageException { |
|||
try { |
|||
Flyway flyway = getFlyway(storage, schema, password); |
|||
flyway.clean(); |
|||
flyway.migrate(); |
|||
} catch(FlywayException e) { |
|||
log.error("Failed to save wallet file.", e); |
|||
throw new StorageException("Failed to save wallet file.\n" + e.getMessage(), e); |
|||
} |
|||
} |
|||
|
|||
private String getSchema(Wallet wallet) { |
|||
return wallet.isMasterWallet() ? MASTER_SCHEMA : WALLET_SCHEMA_PREFIX + wallet.getName(); |
|||
} |
|||
|
|||
private String getFilePassword(ECKey encryptionKey) { |
|||
if(encryptionKey == null) { |
|||
return null; |
|||
} |
|||
|
|||
return Utils.bytesToHex(encryptionKey.getPubKey()); |
|||
} |
|||
|
|||
private void writeBinaryHeader(File walletFile) throws IOException { |
|||
ByteBuffer header = ByteBuffer.allocate(HEADER_MAGIC_1.length + SALT_LENGTH_BYTES); |
|||
header.put(HEADER_MAGIC_1); |
|||
header.put(keyDeriver.getSalt()); |
|||
header.flip(); |
|||
|
|||
try(FileChannel fileChannel = new RandomAccessFile(walletFile, "rwd").getChannel()) { |
|||
fileChannel.position(H2_ENCRYPT_HEADER.length + H2_ENCRYPT_SALT_LENGTH_BYTES); |
|||
fileChannel.write(header); |
|||
} |
|||
} |
|||
|
|||
private void updatePassword(Storage storage, ECKey encryptionPubKey) { |
|||
String newPassword = getFilePassword(encryptionPubKey); |
|||
String currentPassword = getDatasourcePassword(); |
|||
|
|||
//The password only needs to be changed if the datasource is null - either we have loaded the wallet from a datasource, or it is a new wallet and the datasource is still to be created
|
|||
if(dataSource != null && !Objects.equals(currentPassword, newPassword)) { |
|||
if(!dataSource.isClosed()) { |
|||
dataSource.close(); |
|||
} |
|||
|
|||
try { |
|||
File walletFile = storage.getWalletFile(); |
|||
ChangeFileEncryption.execute(walletFile.getParent(), getWalletName(walletFile, null), "AES", |
|||
currentPassword == null ? null : currentPassword.toCharArray(), |
|||
newPassword == null ? null : newPassword.toCharArray(), true); |
|||
|
|||
if(newPassword != null) { |
|||
writeBinaryHeader(walletFile); |
|||
} |
|||
|
|||
//This sets the new password on the datasource for the next updatePassword check
|
|||
getDataSource(storage, newPassword); |
|||
} catch(Exception e) { |
|||
log.error("Error changing database password", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private String getDatasourcePassword() { |
|||
if(dataSource != null) { |
|||
String dsPassword = dataSource.getPassword(); |
|||
if(dsPassword.isEmpty()) { |
|||
return null; |
|||
} |
|||
|
|||
return dsPassword.substring(0, dsPassword.length() - (" " + H2_PASSWORD).length()); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
@Override |
|||
public ECKey getEncryptionKey(CharSequence password) throws IOException { |
|||
return getEncryptionKey(password, null, null); |
|||
} |
|||
|
|||
private ECKey getEncryptionKey(CharSequence password, File walletFile, ECKey alreadyDerivedKey) throws IOException { |
|||
if(password != null && password.equals("")) { |
|||
return Storage.NO_PASSWORD_KEY; |
|||
} |
|||
|
|||
AsymmetricKeyDeriver keyDeriver = getKeyDeriver(walletFile); |
|||
if(alreadyDerivedKey != null) { |
|||
return alreadyDerivedKey; |
|||
} |
|||
|
|||
return password == null ? null : keyDeriver.deriveECKey(password); |
|||
} |
|||
|
|||
@Override |
|||
public AsymmetricKeyDeriver getKeyDeriver() { |
|||
return keyDeriver; |
|||
} |
|||
|
|||
@Override |
|||
public void setKeyDeriver(AsymmetricKeyDeriver keyDeriver) { |
|||
this.keyDeriver = keyDeriver; |
|||
} |
|||
|
|||
private AsymmetricKeyDeriver getKeyDeriver(File walletFile) throws IOException { |
|||
if(keyDeriver == null) { |
|||
keyDeriver = getWalletKeyDeriver(walletFile); |
|||
} |
|||
|
|||
return keyDeriver; |
|||
} |
|||
|
|||
private AsymmetricKeyDeriver getWalletKeyDeriver(File walletFile) throws IOException { |
|||
if(keyDeriver == null) { |
|||
byte[] salt = new byte[SALT_LENGTH_BYTES]; |
|||
|
|||
if(walletFile != null && walletFile.exists()) { |
|||
try(InputStream inputStream = new FileInputStream(walletFile)) { |
|||
inputStream.skip(H2_ENCRYPT_HEADER.length + H2_ENCRYPT_SALT_LENGTH_BYTES + HEADER_MAGIC_1.length); |
|||
inputStream.read(salt, 0, salt.length); |
|||
} |
|||
} else { |
|||
SecureRandom secureRandom = new SecureRandom(); |
|||
secureRandom.nextBytes(salt); |
|||
} |
|||
|
|||
return new Argon2KeyDeriver(salt); |
|||
} |
|||
|
|||
return keyDeriver; |
|||
} |
|||
|
|||
@Override |
|||
public boolean isEncrypted(File walletFile) throws IOException { |
|||
byte[] header = new byte[H2_ENCRYPT_HEADER.length]; |
|||
try(InputStream inputStream = new FileInputStream(walletFile)) { |
|||
inputStream.read(header, 0, H2_ENCRYPT_HEADER.length); |
|||
return Arrays.equals(H2_ENCRYPT_HEADER, header); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public String getWalletId(Storage storage, Wallet wallet) { |
|||
return storage.getWalletFile().getParentFile().getAbsolutePath() + File.separator + getWalletName(storage.getWalletFile(), null) + ":" + (wallet == null || wallet.isMasterWallet() ? "master" : wallet.getName()); |
|||
} |
|||
|
|||
@Override |
|||
public String getWalletName(File walletFile, Wallet wallet) { |
|||
if(wallet != null && wallet.getMasterWallet() != null) { |
|||
return wallet.getName(); |
|||
} |
|||
|
|||
String name = walletFile.getName(); |
|||
if(name.endsWith("." + getType().getExtension())) { |
|||
name = name.substring(0, name.length() - getType().getExtension().length() - 1); |
|||
} |
|||
|
|||
return name; |
|||
} |
|||
|
|||
@Override |
|||
public PersistenceType getType() { |
|||
return PersistenceType.DB; |
|||
} |
|||
|
|||
@Override |
|||
public void copyWallet(File walletFile, OutputStream outputStream) throws IOException { |
|||
if(dataSource != null && !dataSource.isClosed()) { |
|||
dataSource.close(); |
|||
} |
|||
|
|||
com.google.common.io.Files.copy(walletFile, outputStream); |
|||
} |
|||
|
|||
@Override |
|||
public void close() { |
|||
EventManager.get().unregister(this); |
|||
if(dataSource != null && !dataSource.isClosed()) { |
|||
dataSource.close(); |
|||
} |
|||
} |
|||
|
|||
private Jdbi getJdbi(Storage storage, String password) throws StorageException { |
|||
Jdbi jdbi = Jdbi.create(getDataSource(storage, password)); |
|||
jdbi.installPlugin(new H2DatabasePlugin()); |
|||
jdbi.installPlugin(new SqlObjectPlugin()); |
|||
|
|||
return jdbi; |
|||
} |
|||
|
|||
private Flyway getFlyway(Storage storage, String schema, String password) throws StorageException { |
|||
return Flyway.configure().dataSource(getDataSource(storage, password)).locations("com/sparrowwallet/sparrow/sql").schemas(schema).load(); |
|||
} |
|||
|
|||
private HikariDataSource getDataSource(Storage storage, String password) throws StorageException { |
|||
if(dataSource == null || dataSource.isClosed()) { |
|||
dataSource = createDataSource(storage.getWalletFile(), password); |
|||
} |
|||
|
|||
return dataSource; |
|||
} |
|||
|
|||
private HikariDataSource createDataSource(File walletFile, String password) throws StorageException { |
|||
try { |
|||
HikariConfig config = new HikariConfig(); |
|||
config.setJdbcUrl(getUrl(walletFile, password)); |
|||
config.setUsername(H2_USER); |
|||
config.setPassword(password == null ? H2_PASSWORD : password + " " + H2_PASSWORD); |
|||
return new HikariDataSource(config); |
|||
} catch(HikariPool.PoolInitializationException e) { |
|||
if(e.getMessage() != null && e.getMessage().contains("Database may be already in use")) { |
|||
log.error("Wallet file may already be in use. Make sure the application is not running elsewhere.", e); |
|||
throw new StorageException("Wallet file may already be in use. Make sure the application is not running elsewhere.", e); |
|||
} else if(e.getMessage() != null && (e.getMessage().contains("Wrong user name or password") || e.getMessage().contains("Encryption error in file"))) { |
|||
throw new InvalidPasswordException("Incorrect password for wallet file.", e); |
|||
} else { |
|||
log.error("Failed to open database file", e); |
|||
throw new StorageException("Failed to open database file.\n" + e.getMessage(), e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private String getUrl(File walletFile, String password) { |
|||
return "jdbc:h2:" + walletFile.getAbsolutePath().replace("." + getType().getExtension(), "") + ";INIT=SET TRACE_LEVEL_FILE=4;TRACE_LEVEL_FILE=4;DATABASE_TO_UPPER=false" + (password == null ? "" : ";CIPHER=AES"); |
|||
} |
|||
|
|||
private boolean persistsFor(Wallet wallet) { |
|||
if(masterWallet != null) { |
|||
if(wallet == masterWallet) { |
|||
return true; |
|||
} |
|||
|
|||
return masterWallet.getChildWallets().contains(wallet); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
@Subscribe |
|||
public void walletHistoryCleared(WalletHistoryClearedEvent event) { |
|||
if(persistsFor(event.getWallet())) { |
|||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).clearHistory = true; |
|||
} |
|||
} |
|||
|
|||
@Subscribe |
|||
public void walletHistoryChanged(WalletHistoryChangedEvent event) { |
|||
if(persistsFor(event.getWallet())) { |
|||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).historyNodes.addAll(event.getHistoryChangedNodes()); |
|||
} |
|||
} |
|||
|
|||
@Subscribe |
|||
public void walletBlockHeightChanged(WalletBlockHeightChangedEvent event) { |
|||
if(persistsFor(event.getWallet())) { |
|||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).blockHeight = event.getBlockHeight(); |
|||
} |
|||
} |
|||
|
|||
@Subscribe |
|||
public void walletEntryLabelsChanged(WalletEntryLabelsChangedEvent event) { |
|||
if(persistsFor(event.getWallet())) { |
|||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).labelEntries.addAll(event.getEntries()); |
|||
} |
|||
} |
|||
|
|||
@Subscribe |
|||
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) { |
|||
if(persistsFor(event.getWallet())) { |
|||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).utxoStatuses.add(event.getUtxo()); |
|||
} |
|||
} |
|||
|
|||
@Subscribe |
|||
public void keystoreLabelsChanged(KeystoreLabelsChangedEvent event) { |
|||
if(persistsFor(event.getWallet())) { |
|||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).labelKeystores.addAll(event.getChangedKeystores()); |
|||
} |
|||
} |
|||
|
|||
private static class DirtyPersistables { |
|||
public boolean clearHistory; |
|||
public final List<WalletNode> historyNodes = new ArrayList<>(); |
|||
public Integer blockHeight = null; |
|||
public final List<Entry> labelEntries = new ArrayList<>(); |
|||
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>(); |
|||
public final List<Keystore> labelKeystores = new ArrayList<>(); |
|||
|
|||
public String toString() { |
|||
return "Dirty Persistables" + |
|||
"\nClear history:" + clearHistory + |
|||
"\nNodes:" + historyNodes + |
|||
"\nBlockHeight:" + blockHeight + |
|||
"\nTx labels:" + labelEntries.stream().filter(entry -> entry instanceof TransactionEntry).map(entry -> ((TransactionEntry)entry).getBlockTransaction().getHash().toString()).collect(Collectors.toList()) + |
|||
"\nAddress labels:" + labelEntries.stream().filter(entry -> entry instanceof NodeEntry).map(entry -> ((NodeEntry)entry).getNode().toString() + " " + entry.getLabel()).collect(Collectors.toList()) + |
|||
"\nUTXO labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) + |
|||
"\nUTXO statuses:" + utxoStatuses + |
|||
"\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,74 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.sparrowwallet.drongo.crypto.EncryptedData; |
|||
import com.sparrowwallet.drongo.wallet.DeterministicSeed; |
|||
import com.sparrowwallet.drongo.wallet.Keystore; |
|||
import com.sparrowwallet.drongo.wallet.MasterPrivateExtendedKey; |
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper; |
|||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; |
|||
import org.jdbi.v3.sqlobject.statement.SqlQuery; |
|||
import org.jdbi.v3.sqlobject.statement.SqlUpdate; |
|||
|
|||
import java.util.List; |
|||
|
|||
public interface KeystoreDao { |
|||
@SqlQuery("select keystore.id, keystore.label, keystore.source, keystore.walletModel, keystore.masterFingerprint, keystore.derivationPath, keystore.extendedPublicKey, " + |
|||
"masterPrivateExtendedKey.id, masterPrivateExtendedKey.privateKey, masterPrivateExtendedKey.chainCode, masterPrivateExtendedKey.initialisationVector, masterPrivateExtendedKey.encryptedBytes, masterPrivateExtendedKey.keySalt, masterPrivateExtendedKey.deriver, masterPrivateExtendedKey.crypter, " + |
|||
"seed.id, seed.type, seed.mnemonicString, seed.initialisationVector, seed.encryptedBytes, seed.keySalt, seed.deriver, seed.crypter, seed.needsPassphrase, seed.creationTimeSeconds " + |
|||
"from keystore left join masterPrivateExtendedKey on keystore.masterPrivateExtendedKey = masterPrivateExtendedKey.id left join seed on keystore.seed = seed.id where keystore.wallet = ? order by keystore.index asc") |
|||
@RegisterRowMapper(KeystoreMapper.class) |
|||
List<Keystore> getForWalletId(Long id); |
|||
|
|||
@SqlUpdate("insert into keystore (label, source, walletModel, masterFingerprint, derivationPath, extendedPublicKey, masterPrivateExtendedKey, seed, wallet, index) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") |
|||
@GetGeneratedKeys("id") |
|||
long insert(String label, int source, int walletModel, String masterFingerprint, String derivationPath, String extendedPublicKey, Long masterPrivateExtendedKey, Long seed, long wallet, int index); |
|||
|
|||
@SqlUpdate("insert into masterPrivateExtendedKey (privateKey, chainCode, initialisationVector, encryptedBytes, keySalt, deriver, crypter, creationTimeSeconds) values (?, ?, ?, ?, ?, ?, ?, ?)") |
|||
@GetGeneratedKeys("id") |
|||
long insertMasterPrivateExtendedKey(byte[] privateKey, byte[] chainCode, byte[] initialisationVector, byte[] encryptedBytes, byte[] keySalt, Integer deriver, Integer crypter, long creationTimeSeconds); |
|||
|
|||
@SqlUpdate("insert into seed (type, mnemonicString, initialisationVector, encryptedBytes, keySalt, deriver, crypter, needsPassphrase, creationTimeSeconds) values (?, ?, ?, ?, ?, ?, ?, ?, ?)") |
|||
@GetGeneratedKeys("id") |
|||
long insertSeed(int type, String mnemonicString, byte[] initialisationVector, byte[] encryptedBytes, byte[] keySalt, Integer deriver, Integer crypter, boolean needsPassphrase, long creationTimeSeconds); |
|||
|
|||
@SqlUpdate("update keystore set label = ? where id = ?") |
|||
void updateLabel(String label, long id); |
|||
|
|||
default void addKeystores(Wallet wallet) { |
|||
for(int i = 0; i < wallet.getKeystores().size(); i++) { |
|||
Keystore keystore = wallet.getKeystores().get(i); |
|||
if(keystore.getMasterPrivateExtendedKey() != null) { |
|||
MasterPrivateExtendedKey mpek = keystore.getMasterPrivateExtendedKey(); |
|||
if(mpek.isEncrypted()) { |
|||
EncryptedData data = mpek.getEncryptedData(); |
|||
long id = insertMasterPrivateExtendedKey(null, null, data.getInitialisationVector(), data.getEncryptedBytes(), data.getKeySalt(), data.getEncryptionType().getDeriver().ordinal(), data.getEncryptionType().getCrypter().ordinal(), mpek.getCreationTimeSeconds()); |
|||
mpek.setId(id); |
|||
} else { |
|||
long id = insertMasterPrivateExtendedKey(mpek.getPrivateKey().getPrivKeyBytes(), mpek.getPrivateKey().getChainCode(), null, null, null, null, null, mpek.getCreationTimeSeconds()); |
|||
mpek.setId(id); |
|||
} |
|||
} |
|||
|
|||
if(keystore.getSeed() != null) { |
|||
DeterministicSeed seed = keystore.getSeed(); |
|||
if(seed.isEncrypted()) { |
|||
EncryptedData data = seed.getEncryptedData(); |
|||
long id = insertSeed(seed.getType().ordinal(), null, data.getInitialisationVector(), data.getEncryptedBytes(), data.getKeySalt(), data.getEncryptionType().getDeriver().ordinal(), data.getEncryptionType().getCrypter().ordinal(), seed.needsPassphrase(), seed.getCreationTimeSeconds()); |
|||
seed.setId(id); |
|||
} else { |
|||
long id = insertSeed(seed.getType().ordinal(), seed.getMnemonicString().asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds()); |
|||
seed.setId(id); |
|||
} |
|||
} |
|||
|
|||
long id = insert(keystore.getLabel(), keystore.getSource().ordinal(), keystore.getWalletModel().ordinal(), |
|||
keystore.hasPrivateKey() ? null : keystore.getKeyDerivation().getMasterFingerprint(), |
|||
keystore.getKeyDerivation().getDerivationPath(), |
|||
keystore.hasPrivateKey() ? null : keystore.getExtendedPublicKey().toString(), |
|||
keystore.getMasterPrivateExtendedKey() == null ? null : keystore.getMasterPrivateExtendedKey().getId(), |
|||
keystore.getSeed() == null ? null : keystore.getSeed().getId(), wallet.getId(), i); |
|||
keystore.setId(id); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,58 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.sparrowwallet.drongo.ExtendedKey; |
|||
import com.sparrowwallet.drongo.KeyDerivation; |
|||
import com.sparrowwallet.drongo.crypto.EncryptedData; |
|||
import com.sparrowwallet.drongo.crypto.EncryptionType; |
|||
import com.sparrowwallet.drongo.wallet.*; |
|||
import org.jdbi.v3.core.mapper.RowMapper; |
|||
import org.jdbi.v3.core.statement.StatementContext; |
|||
|
|||
import java.sql.ResultSet; |
|||
import java.sql.SQLException; |
|||
import java.util.Arrays; |
|||
import java.util.List; |
|||
|
|||
public class KeystoreMapper implements RowMapper<Keystore> { |
|||
|
|||
@Override |
|||
public Keystore map(ResultSet rs, StatementContext ctx) throws SQLException { |
|||
Keystore keystore = new Keystore(rs.getString("keystore.label")); |
|||
keystore.setId(rs.getLong("keystore.id")); |
|||
keystore.setSource(KeystoreSource.values()[rs.getInt("keystore.source")]); |
|||
keystore.setWalletModel(WalletModel.values()[rs.getInt("keystore.walletModel")]); |
|||
keystore.setKeyDerivation(new KeyDerivation(rs.getString("keystore.masterFingerprint"), rs.getString("keystore.derivationPath"))); |
|||
keystore.setExtendedPublicKey(rs.getString("keystore.extendedPublicKey") == null ? null : ExtendedKey.fromDescriptor(rs.getString("keystore.extendedPublicKey"))); |
|||
|
|||
if(rs.getBytes("masterPrivateExtendedKey.privateKey") != null) { |
|||
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(rs.getBytes("masterPrivateExtendedKey.privateKey"), rs.getBytes("masterPrivateExtendedKey.chainCode")); |
|||
masterPrivateExtendedKey.setId(rs.getLong("masterPrivateExtendedKey.id")); |
|||
keystore.setMasterPrivateExtendedKey(masterPrivateExtendedKey); |
|||
} else if(rs.getBytes("masterPrivateExtendedKey.encryptedBytes") != null) { |
|||
EncryptedData encryptedData = new EncryptedData(rs.getBytes("masterPrivateExtendedKey.initialisationVector"), |
|||
rs.getBytes("masterPrivateExtendedKey.encryptedBytes"), rs.getBytes("masterPrivateExtendedKey.keySalt"), |
|||
EncryptionType.Deriver.values()[rs.getInt("masterPrivateExtendedKey.deriver")], |
|||
EncryptionType.Crypter.values()[rs.getInt("masterPrivateExtendedKey.crypter")]); |
|||
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(encryptedData); |
|||
masterPrivateExtendedKey.setId(rs.getLong("masterPrivateExtendedKey.id")); |
|||
keystore.setMasterPrivateExtendedKey(masterPrivateExtendedKey); |
|||
} |
|||
|
|||
if(rs.getString("seed.mnemonicString") != null) { |
|||
List<String> mnemonicCode = Arrays.asList(rs.getString("seed.mnemonicString").split(" ")); |
|||
DeterministicSeed seed = new DeterministicSeed(mnemonicCode, rs.getBoolean("seed.needsPassphrase"), rs.getLong("seed.creationTimeSeconds"), DeterministicSeed.Type.values()[rs.getInt("seed.type")]); |
|||
seed.setId(rs.getLong("seed.id")); |
|||
keystore.setSeed(seed); |
|||
} else if(rs.getBytes("seed.encryptedBytes") != null) { |
|||
EncryptedData encryptedData = new EncryptedData(rs.getBytes("seed.initialisationVector"), |
|||
rs.getBytes("seed.encryptedBytes"), rs.getBytes("seed.keySalt"), |
|||
EncryptionType.Deriver.values()[rs.getInt("seed.deriver")], |
|||
EncryptionType.Crypter.values()[rs.getInt("seed.crypter")]); |
|||
DeterministicSeed seed = new DeterministicSeed(encryptedData, rs.getBoolean("seed.needsPassphrase"), rs.getLong("seed.creationTimeSeconds"), DeterministicSeed.Type.values()[rs.getInt("seed.type")]); |
|||
seed.setId(rs.getLong("seed.id")); |
|||
keystore.setSeed(seed); |
|||
} |
|||
|
|||
return keystore; |
|||
} |
|||
} |
@ -0,0 +1,22 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.sparrowwallet.drongo.policy.Policy; |
|||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper; |
|||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; |
|||
import org.jdbi.v3.sqlobject.statement.SqlQuery; |
|||
import org.jdbi.v3.sqlobject.statement.SqlUpdate; |
|||
|
|||
public interface PolicyDao { |
|||
@SqlQuery("select * from policy where id = ?") |
|||
@RegisterRowMapper(PolicyMapper.class) |
|||
Policy getPolicy(long id); |
|||
|
|||
@SqlUpdate("insert into policy (name, script) values (?, ?)") |
|||
@GetGeneratedKeys("id") |
|||
long insert(String name, String script); |
|||
|
|||
default void addPolicy(Policy policy) { |
|||
long id = insert(policy.getName(), policy.getMiniscript().getScript()); |
|||
policy.setId(id); |
|||
} |
|||
} |
@ -0,0 +1,18 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.sparrowwallet.drongo.policy.Miniscript; |
|||
import com.sparrowwallet.drongo.policy.Policy; |
|||
import org.jdbi.v3.core.mapper.RowMapper; |
|||
import org.jdbi.v3.core.statement.StatementContext; |
|||
|
|||
import java.sql.ResultSet; |
|||
import java.sql.SQLException; |
|||
|
|||
public class PolicyMapper implements RowMapper<Policy> { |
|||
@Override |
|||
public Policy map(ResultSet rs, StatementContext ctx) throws SQLException { |
|||
Policy policy = new Policy(rs.getString("name"), new Miniscript(rs.getString("script"))); |
|||
policy.setId(rs.getLong("id")); |
|||
return policy; |
|||
} |
|||
} |
@ -0,0 +1,106 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.sparrowwallet.drongo.protocol.Sha256Hash; |
|||
import com.sparrowwallet.drongo.wallet.BlockTransaction; |
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
import com.sparrowwallet.drongo.wallet.WalletNode; |
|||
import org.jdbi.v3.sqlobject.CreateSqlObject; |
|||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper; |
|||
import org.jdbi.v3.sqlobject.customizer.Bind; |
|||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; |
|||
import org.jdbi.v3.sqlobject.statement.SqlQuery; |
|||
import org.jdbi.v3.sqlobject.statement.SqlUpdate; |
|||
|
|||
import java.util.Date; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.stream.Collectors; |
|||
|
|||
public interface WalletDao { |
|||
@CreateSqlObject |
|||
PolicyDao createPolicyDao(); |
|||
|
|||
@CreateSqlObject |
|||
KeystoreDao createKeystoreDao(); |
|||
|
|||
@CreateSqlObject |
|||
WalletNodeDao createWalletNodeDao(); |
|||
|
|||
@CreateSqlObject |
|||
BlockTransactionDao createBlockTransactionDao(); |
|||
|
|||
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id") |
|||
@RegisterRowMapper(WalletMapper.class) |
|||
List<Wallet> loadAllWallets(); |
|||
|
|||
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id = 1") |
|||
@RegisterRowMapper(WalletMapper.class) |
|||
Wallet loadMainWallet(); |
|||
|
|||
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id != 1") |
|||
@RegisterRowMapper(WalletMapper.class) |
|||
List<Wallet> loadChildWallets(); |
|||
|
|||
@SqlUpdate("insert into wallet (name, network, policyType, scriptType, storedBlockHeight, gapLimit, birthDate, defaultPolicy) values (?, ?, ?, ?, ?, ?, ?, ?)") |
|||
@GetGeneratedKeys("id") |
|||
long insert(String name, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Date birthDate, long defaultPolicy); |
|||
|
|||
@SqlUpdate("update wallet set storedBlockHeight = :blockHeight where id = :id") |
|||
void updateStoredBlockHeight(@Bind("id") long id, @Bind("blockHeight") Integer blockHeight); |
|||
|
|||
@SqlUpdate("set schema ?") |
|||
int setSchema(String schema); |
|||
|
|||
default Wallet getMainWallet(String schema) { |
|||
try { |
|||
setSchema(schema); |
|||
Wallet mainWallet = loadMainWallet(); |
|||
if(mainWallet != null) { |
|||
loadWallet(mainWallet); |
|||
} |
|||
|
|||
return mainWallet; |
|||
} finally { |
|||
setSchema(DbPersistence.DEFAULT_SCHEMA); |
|||
} |
|||
} |
|||
|
|||
default List<Wallet> getChildWallets(String schema) { |
|||
try { |
|||
List<Wallet> childWallets = loadChildWallets(); |
|||
for(Wallet childWallet : childWallets) { |
|||
loadWallet(childWallet); |
|||
} |
|||
|
|||
return childWallets; |
|||
} finally { |
|||
setSchema(DbPersistence.DEFAULT_SCHEMA); |
|||
} |
|||
} |
|||
|
|||
default void loadWallet(Wallet wallet) { |
|||
wallet.getKeystores().addAll(createKeystoreDao().getForWalletId(wallet.getId())); |
|||
|
|||
List<WalletNode> walletNodes = createWalletNodeDao().getForWalletId(wallet.getId()); |
|||
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList())); |
|||
|
|||
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId()); //.stream().collect(Collectors.toMap(BlockTransaction::getHash, Function.identity(), (existing, replacement) -> existing, LinkedHashMap::new));
|
|||
wallet.updateTransactions(blockTransactions); |
|||
} |
|||
|
|||
default void addWallet(String schema, Wallet wallet) { |
|||
try { |
|||
setSchema(schema); |
|||
createPolicyDao().addPolicy(wallet.getDefaultPolicy()); |
|||
|
|||
long id = insert(wallet.getName(), wallet.getNetwork().ordinal(), wallet.getPolicyType().ordinal(), wallet.getScriptType().ordinal(), wallet.getStoredBlockHeight(), wallet.gapLimit(), wallet.getBirthDate(), wallet.getDefaultPolicy().getId()); |
|||
wallet.setId(id); |
|||
|
|||
createKeystoreDao().addKeystores(wallet); |
|||
createWalletNodeDao().addWalletNodes(wallet); |
|||
createBlockTransactionDao().addBlockTransactions(wallet); |
|||
} finally { |
|||
setSchema(DbPersistence.DEFAULT_SCHEMA); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,37 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.sparrowwallet.drongo.Network; |
|||
import com.sparrowwallet.drongo.policy.Miniscript; |
|||
import com.sparrowwallet.drongo.policy.Policy; |
|||
import com.sparrowwallet.drongo.policy.PolicyType; |
|||
import com.sparrowwallet.drongo.protocol.ScriptType; |
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
import org.jdbi.v3.core.mapper.RowMapper; |
|||
import org.jdbi.v3.core.statement.StatementContext; |
|||
|
|||
import java.sql.ResultSet; |
|||
import java.sql.SQLException; |
|||
|
|||
public class WalletMapper implements RowMapper<Wallet> { |
|||
@Override |
|||
public Wallet map(ResultSet rs, StatementContext ctx) throws SQLException { |
|||
Wallet wallet = new Wallet(rs.getString("wallet.name")); |
|||
wallet.setId(rs.getLong("wallet.id")); |
|||
wallet.setNetwork(Network.values()[rs.getInt("wallet.network")]); |
|||
wallet.setPolicyType(PolicyType.values()[rs.getInt("wallet.policyType")]); |
|||
wallet.setScriptType(ScriptType.values()[rs.getInt("wallet.scriptType")]); |
|||
|
|||
Policy policy = new Policy(rs.getString("policy.name"), new Miniscript(rs.getString("policy.script"))); |
|||
policy.setId(rs.getLong("policy.id")); |
|||
wallet.setDefaultPolicy(policy); |
|||
|
|||
int storedBlockHeight = rs.getInt("wallet.storedBlockHeight"); |
|||
wallet.setStoredBlockHeight(rs.wasNull() ? null : storedBlockHeight); |
|||
|
|||
int gapLimit = rs.getInt("wallet.gapLimit"); |
|||
wallet.gapLimit(rs.wasNull() ? null : gapLimit); |
|||
wallet.setBirthDate(rs.getTimestamp("wallet.birthDate")); |
|||
|
|||
return wallet; |
|||
} |
|||
} |
@ -0,0 +1,116 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; |
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
import com.sparrowwallet.drongo.wallet.WalletNode; |
|||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper; |
|||
import org.jdbi.v3.sqlobject.customizer.Bind; |
|||
import org.jdbi.v3.sqlobject.customizer.BindList; |
|||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; |
|||
import org.jdbi.v3.sqlobject.statement.SqlQuery; |
|||
import org.jdbi.v3.sqlobject.statement.SqlUpdate; |
|||
import org.jdbi.v3.sqlobject.statement.UseRowReducer; |
|||
|
|||
import java.util.Date; |
|||
import java.util.List; |
|||
|
|||
public interface WalletNodeDao { |
|||
@SqlQuery("select walletNode.id, walletNode.derivationPath, walletNode.label, walletNode.parent, " + |
|||
"blockTransactionHashIndex.id, blockTransactionHashIndex.hash, blockTransactionHashIndex.height, blockTransactionHashIndex.date, blockTransactionHashIndex.fee, blockTransactionHashIndex.label, " + |
|||
"blockTransactionHashIndex.index, blockTransactionHashIndex.value, blockTransactionHashIndex.status, blockTransactionHashIndex.spentBy, blockTransactionHashIndex.node " + |
|||
"from walletNode left join blockTransactionHashIndex on walletNode.id = blockTransactionHashIndex.node where walletNode.wallet = ? order by walletNode.parent asc nulls first, blockTransactionHashIndex.spentBy asc nulls first") |
|||
@RegisterRowMapper(WalletNodeMapper.class) |
|||
@RegisterRowMapper(BlockTransactionHashIndexMapper.class) |
|||
@UseRowReducer(WalletNodeReducer.class) |
|||
List<WalletNode> getForWalletId(Long id); |
|||
|
|||
@SqlUpdate("insert into walletNode (derivationPath, label, wallet, parent) values (?, ?, ?, ?)") |
|||
@GetGeneratedKeys("id") |
|||
long insertWalletNode(String derivationPath, String label, long wallet, Long parent); |
|||
|
|||
@SqlUpdate("insert into blockTransactionHashIndex (hash, height, date, fee, label, index, value, status, spentBy, node) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") |
|||
@GetGeneratedKeys("id") |
|||
long insertBlockTransactionHashIndex(byte[] hash, int height, Date date, Long fee, String label, long index, long value, Integer status, Long spentBy, long node); |
|||
|
|||
@SqlUpdate("update blockTransactionHashIndex set hash = ?, height = ?, date = ?, fee = ?, label = ?, index = ?, value = ?, status = ?, spentBy = ?, node = ? where id = ?") |
|||
void updateBlockTransactionHashIndex(byte[] hash, int height, Date date, Long fee, String label, long index, long value, Integer status, Long spentBy, long node, long id); |
|||
|
|||
@SqlUpdate("update walletNode set label = :label where id = :id") |
|||
void updateNodeLabel(@Bind("id") long id, @Bind("label") String label); |
|||
|
|||
@SqlUpdate("update blockTransactionHashIndex set label = :label where id = :id") |
|||
void updateTxoLabel(@Bind("id") long id, @Bind("label") String label); |
|||
|
|||
@SqlUpdate("update blockTransactionHashIndex set status = :status where id = :id") |
|||
void updateTxoStatus(@Bind("id") long id, @Bind("status") Integer status); |
|||
|
|||
@SqlUpdate("delete from blockTransactionHashIndex where blockTransactionHashIndex.node in (select walletNode.id from walletNode where walletNode.wallet = ?)") |
|||
void clearHistory(long wallet); |
|||
|
|||
@SqlUpdate("delete from blockTransactionHashIndex where blockTransactionHashIndex.node in (select walletNode.id from walletNode where walletNode.wallet = ?) and blockTransactionHashIndex.spentBy is not null") |
|||
void clearSpentHistory(long wallet); |
|||
|
|||
@SqlUpdate("delete from blockTransactionHashIndex where node = :nodeId and id not in (<ids>)") |
|||
void deleteUnreferencedNodeTxos(@Bind("nodeId") Long nodeId, @BindList("ids") List<Long> ids); |
|||
|
|||
@SqlUpdate("delete from blockTransactionHashIndex where node = :nodeId and id not in (<ids>) and spentBy is not null") |
|||
void deleteUnreferencedNodeSpentTxos(@Bind("nodeId") Long nodeId, @BindList("ids") List<Long> ids); |
|||
|
|||
default void addWalletNodes(Wallet wallet) { |
|||
for(WalletNode purposeNode : wallet.getPurposeNodes()) { |
|||
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null); |
|||
purposeNode.setId(purposeNodeId); |
|||
for(WalletNode addressNode : purposeNode.getChildren()) { |
|||
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNodeId); |
|||
addressNode.setId(addressNodeId); |
|||
addTransactionOutputs(addressNode); |
|||
} |
|||
} |
|||
} |
|||
|
|||
default void addTransactionOutputs(WalletNode addressNode) { |
|||
for(BlockTransactionHashIndex txo : addressNode.getTransactionOutputs()) { |
|||
txo.setId(null); |
|||
if(txo.isSpent()) { |
|||
txo.getSpentBy().setId(null); |
|||
} |
|||
|
|||
addOrUpdate(addressNode, txo); |
|||
} |
|||
} |
|||
|
|||
default void addOrUpdate(WalletNode addressNode, BlockTransactionHashIndex txo) { |
|||
Long spentById = null; |
|||
if(txo.isSpent()) { |
|||
BlockTransactionHashIndex spentBy = txo.getSpentBy(); |
|||
if(spentBy.getId() == null) { |
|||
spentById = insertBlockTransactionHashIndex(spentBy.getHash().getBytes(), spentBy.getHeight(), spentBy.getDate(), spentBy.getFee(), spentBy.getLabel(), spentBy.getIndex(), spentBy.getValue(), |
|||
spentBy.getStatus() == null ? null : spentBy.getStatus().ordinal(), null, addressNode.getId()); |
|||
spentBy.setId(spentById); |
|||
} else { |
|||
updateBlockTransactionHashIndex(spentBy.getHash().getBytes(), spentBy.getHeight(), spentBy.getDate(), spentBy.getFee(), spentBy.getLabel(), spentBy.getIndex(), spentBy.getValue(), |
|||
spentBy.getStatus() == null ? null : spentBy.getStatus().ordinal(), null, addressNode.getId(), spentBy.getId()); |
|||
spentById = spentBy.getId(); |
|||
} |
|||
} |
|||
|
|||
if(txo.getId() == null) { |
|||
long txoId = insertBlockTransactionHashIndex(txo.getHash().getBytes(), txo.getHeight(), txo.getDate(), txo.getFee(), txo.getLabel(), txo.getIndex(), txo.getValue(), |
|||
txo.getStatus() == null ? null : txo.getStatus().ordinal(), spentById, addressNode.getId()); |
|||
txo.setId(txoId); |
|||
} else { |
|||
updateBlockTransactionHashIndex(txo.getHash().getBytes(), txo.getHeight(), txo.getDate(), txo.getFee(), txo.getLabel(), txo.getIndex(), txo.getValue(), |
|||
txo.getStatus() == null ? null : txo.getStatus().ordinal(), spentById, addressNode.getId(), txo.getId()); |
|||
} |
|||
} |
|||
|
|||
default void deleteNodeTxosNotInList(WalletNode addressNode, List<Long> txoIds) { |
|||
deleteUnreferencedNodeSpentTxos(addressNode.getId(), txoIds); |
|||
deleteUnreferencedNodeTxos(addressNode.getId(), txoIds); |
|||
} |
|||
|
|||
default void clearHistory(Wallet wallet) { |
|||
clearSpentHistory(wallet.getId()); |
|||
clearHistory(wallet.getId()); |
|||
} |
|||
} |
@ -0,0 +1,19 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.sparrowwallet.drongo.wallet.WalletNode; |
|||
import org.jdbi.v3.core.mapper.RowMapper; |
|||
import org.jdbi.v3.core.statement.StatementContext; |
|||
|
|||
import java.sql.ResultSet; |
|||
import java.sql.SQLException; |
|||
|
|||
public class WalletNodeMapper implements RowMapper<WalletNode> { |
|||
@Override |
|||
public WalletNode map(ResultSet rs, StatementContext ctx) throws SQLException { |
|||
WalletNode walletNode = new WalletNode(rs.getString("walletNode.derivationPath")); |
|||
walletNode.setId(rs.getLong("walletNode.id")); |
|||
walletNode.setLabel(rs.getString("walletNode.label")); |
|||
|
|||
return walletNode; |
|||
} |
|||
} |
@ -0,0 +1,30 @@ |
|||
package com.sparrowwallet.sparrow.io.db; |
|||
|
|||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; |
|||
import com.sparrowwallet.drongo.wallet.WalletNode; |
|||
import org.jdbi.v3.core.result.LinkedHashMapRowReducer; |
|||
import org.jdbi.v3.core.result.RowView; |
|||
|
|||
import java.util.Map; |
|||
|
|||
public class WalletNodeReducer implements LinkedHashMapRowReducer<Long, WalletNode> { |
|||
@Override |
|||
public void accumulate(Map<Long, WalletNode> map, RowView rowView) { |
|||
WalletNode walletNode = map.computeIfAbsent(rowView.getColumn("walletNode.id", Long.class), id -> rowView.getRow(WalletNode.class)); |
|||
|
|||
if(rowView.getColumn("walletNode.parent", Long.class) != null) { |
|||
WalletNode parentNode = map.get(rowView.getColumn("walletNode.parent", Long.class)); |
|||
parentNode.getChildren().add(walletNode); |
|||
} |
|||
|
|||
if(rowView.getColumn("blockTransactionHashIndex.node", Long.class) != null) { |
|||
BlockTransactionHashIndex blockTransactionHashIndex = rowView.getRow(BlockTransactionHashIndex.class); |
|||
if(rowView.getColumn("blockTransactionHashIndex.spentBy", Long.class) != null) { |
|||
BlockTransactionHashIndex spentBy = walletNode.getTransactionOutputs().stream().filter(ref -> ref.getId().equals(rowView.getColumn("blockTransactionHashIndex.spentBy", Long.class))).findFirst().orElseThrow(); |
|||
blockTransactionHashIndex.setSpentBy(spentBy); |
|||
walletNode.getTransactionOutputs().remove(spentBy); |
|||
} |
|||
walletNode.getTransactionOutputs().add(blockTransactionHashIndex); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,22 @@ |
|||
create table blockTransaction (id identity not null, txid binary(32) not null, hash binary(32) not null, height integer not null, date timestamp, fee bigint, label varchar(255), transaction binary, blockHash binary(32), wallet bigint not null); |
|||
create table blockTransactionHashIndex (id identity not null, hash binary(32) not null, height integer not null, date timestamp, fee bigint, label varchar(255), index bigint not null, value bigint not null, status integer, spentBy bigint, node bigint not null); |
|||
create table keystore (id identity not null, label varchar(255), source integer not null, walletModel integer not null, masterFingerprint varchar(8), derivationPath varchar(255) not null, extendedPublicKey varchar(255), masterPrivateExtendedKey bigint, seed bigint, wallet bigint not null, index integer not null); |
|||
create table masterPrivateExtendedKey (id identity not null, privateKey binary(255), chainCode binary(255), initialisationVector binary(255), encryptedBytes binary(255), keySalt binary(255), deriver integer, crypter integer, creationTimeSeconds bigint); |
|||
create table policy (id identity not null, name varchar(255) not null, script varchar(2048) not null); |
|||
create table seed (id identity not null, type integer not null, mnemonicString varchar(255), initialisationVector binary(255), encryptedBytes binary(255), keySalt binary(255), deriver integer, crypter integer, needsPassphrase boolean, creationTimeSeconds bigint); |
|||
create table wallet (id identity not null, name varchar(255) not null, network integer not null, policyType integer not null, scriptType integer not null, storedBlockHeight integer, gapLimit integer, birthDate timestamp, defaultPolicy bigint not null); |
|||
create table walletNode (id identity not null, derivationPath varchar(255) not null, label varchar(255), wallet bigint not null, parent bigint); |
|||
alter table blockTransactionHashIndex add constraint blockTransactionHashIndex_spentBy_unique unique (spentBy); |
|||
alter table keystore add constraint keystore_masterPrivateExtendedKey_unique unique (masterPrivateExtendedKey); |
|||
alter table keystore add constraint keystore_seed_unique unique (seed); |
|||
alter table wallet add constraint wallet_defaultPolicy_unique unique (defaultPolicy); |
|||
alter table blockTransaction add constraint blockTransaction_wallet foreign key (wallet) references wallet; |
|||
alter table blockTransactionHashIndex add constraint blockTransactionHashIndex_spentBy foreign key (spentBy) references blockTransactionHashIndex; |
|||
alter table blockTransactionHashIndex add constraint blockTransactionHashIndex_node foreign key (node) references walletNode; |
|||
alter table keystore add constraint keystore_masterPrivateExtendedKey foreign key (masterPrivateExtendedKey) references masterPrivateExtendedKey; |
|||
alter table keystore add constraint keystore_seed foreign key (seed) references seed; |
|||
alter table keystore add constraint keystore_wallet foreign key (wallet) references wallet; |
|||
alter table wallet add constraint wallet_defaultPolicy foreign key (defaultPolicy) references policy; |
|||
alter table walletNode add constraint walletNode_wallet foreign key (wallet) references wallet; |
|||
alter table walletNode add constraint walletNode_walletNode foreign key (parent) references walletNode; |
|||
create index blockTransaction_txid on blockTransaction(txid); |
Loading…
Reference in new issue