diff --git a/.gitignore b/.gitignore index b3d9d3bf..159d95f4 100644 --- a/.gitignore +++ b/.gitignore @@ -83,5 +83,6 @@ android # Temporary build files rcc_cgo_*.go +darwin diff --git a/.travis.yml b/.travis.yml index 6aaea2df..f4f4ee10 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,89 +1,89 @@ sudo: required language: go go: -- 1.12.6 + - 1.12.6 services: -- docker + - docker go_import_path: github.com/fibercrypto/fibercryptowallet matrix: include: - - stage: RPI - if: branch = release*$ - install: - - DEFAULT_ARCH=rpi1 make install-docker-deps - - DEFAULT_ARCH=rpi2 make install-docker-deps - - DEFAULT_ARCH=rpi3 make install-docker-deps - - make install-deps-no-envs - - make install-coveralls - script: - - make clean - - DEFAULT_TARGET=rpi1 make build-docker - - DEFAULT_TARGET=rpi2 make build-docker - - DEFAULT_TARGET=rpi3 make build-docker - - make lint - - make test - - stage: Android - if: branch = release*$ - install: - - DEFAULT_ARCH=android make install-docker-deps - - make install-deps-no-envs - - make install-coveralls - script: - - make clean - - DEFAULT_TARGET=android make build-docker - - DEFAULT_TARGET=android-emulator make build-docker - - make lint - - make test - - make test-cover-travis - - stage: Linux - install: - - make install-docker-deps - - make install-deps-no-envs - - make install-linters - - make install-coveralls - script: - - make clean - - DEFAULT_TARGET=linux make build-docker - - make test - - make lint - - make test-cover-travis - - stage: Windows - os: windows - install: - - choco install make -y - - travis_wait make install-deps-Windows - - make install-linters - - make install-coveralls - script: - - make build - - make test - - make build-icon - - make lint - - make test-cover-travis - - stage: MacOS - os: osx - osx_image: xcode10.2 - install: - - make install-deps - - make install-coveralls - script: - - make clean - - make build - - make test - - make build-icon - - make lint + - stage: RPI + if: branch = release*$ + install: + - DEFAULT_ARCH=rpi1 make install-docker-deps + - DEFAULT_ARCH=rpi2 make install-docker-deps + - DEFAULT_ARCH=rpi3 make install-docker-deps + - make install-deps-no-envs + - make install-coveralls + script: + - make clean + - DEFAULT_TARGET=rpi1 make build-docker + - DEFAULT_TARGET=rpi2 make build-docker + - DEFAULT_TARGET=rpi3 make build-docker + - make lint + - make test + - stage: Android + if: branch = release*$ + install: + - DEFAULT_ARCH=android make install-docker-deps + - make install-deps-no-envs + - make install-coveralls + script: + - make clean + - DEFAULT_TARGET=android make build-docker + - DEFAULT_TARGET=android-emulator make build-docker + - make lint + - make test - make test-cover-travis + - stage: Linux + install: + - make install-docker-deps + - make install-deps-no-envs + - make install-linters + - make install-coveralls + script: + - make clean + - DEFAULT_TARGET=linux make build-docker + - make test + - make lint + - make test-cover-travis + - stage: Windows + os: windows + install: + - choco install make -y + - travis_wait make install-deps-Windows + - make install-linters + - make install-coveralls + script: + - make build + - make test + - make build-icon + - make lint + - make test-cover-travis + - stage: MacOS + os: osx + osx_image: xcode10.2 + install: + - make install-deps + - make install-coveralls + script: + - make clean + - make build + - make test + - make build-icon + - make lint + - make test-cover-travis notifications: email: false webhooks: https://fathomless-fjord-24024.herokuapp.com/notify before_deploy: - - export VERSION="$(git describe --tags --exact-match HEAD 2> /dev/null)" - - export ARCH="$(uname -m)" - - export OS="$(uname -s)" - - make prepare-release - - make build - - ( cd deploy && tar czf ../fibercryptowallet-${VERSION}-${OS}-${ARCH}.tar.gz ./* ) - - pwd && ls -l . + - export VERSION="$(git describe --tags --exact-match HEAD 2> /dev/null)" + - export ARCH="$(uname -m)" + - export OS="$(uname -s)" + - make prepare-release + - make build + - ( cd deploy && tar czf ../fibercryptowallet-${VERSION}-${OS}-${ARCH}.tar.gz ./* ) + - pwd && ls -l . deploy: provider: releases api_key: diff --git a/.travis/install-golangci-lint.sh b/.travis/install-golangci-lint.sh index ee94ad03..e10f1253 100755 --- a/.travis/install-golangci-lint.sh +++ b/.travis/install-golangci-lint.sh @@ -384,4 +384,4 @@ CHECKSUM=${PROJECT_NAME}-${VERSION}-checksums.txt CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM} -execute \ No newline at end of file +execute diff --git a/CHANGELOG.md b/CHANGELOG.md index e37d8670..180d7b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added +- Spend in single transaction coins owned by multiple wallets (same altcoin plugin) - Added logger for the app and proper error handling - Built-in support for SkyWallet hardware wallet as signer for Skycoin transactions - Built-in support iand GUI for applying operations upon SkyWallet hardware devices diff --git a/Makefile b/Makefile index d74259cb..2a1617db 100644 --- a/Makefile +++ b/Makefile @@ -232,7 +232,6 @@ clean: clean-test clean-build ## Remove temporary files gen-mocks: ## Generate mocks for interface types mockery -all -output src/coin/mocks -outpkg mocks -dir src/core - find src/coin/mocks/ -name '*.go' -type f -print0 test-sky: ## Run Skycoin plugin test suite go test -cover -timeout 30s github.com/fibercrypto/fibercryptowallet/src/coin/skycoin diff --git a/README.md b/README.md index ed6ca019..f8fd564d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Project files are organized as follows: - `./src/coin/skycoin/models` : Skycoin implementation of golang core interfaces. - `./src/coin/skycoin/blockchain` : Skycoin blockchain API. - `./src/coin/skycoin/sign` : Skycoin sign API. -- `vendor` : Project dependencies managed by `dep`. +- `./vendor` : Project dependencies managed by `dep`. ### Architecture diff --git a/qtquickcontrols2.conf b/qtquickcontrols2.conf index f399a31f..341c2f81 100644 --- a/qtquickcontrols2.conf +++ b/qtquickcontrols2.conf @@ -7,4 +7,4 @@ Style = Material [Material] Accent = Blue -Variant = Dense \ No newline at end of file +Variant = Dense diff --git a/resources/fonts/code-new-roman/license.txt b/resources/fonts/code-new-roman/license.txt index 14c043d6..e47e0f90 100644 --- a/resources/fonts/code-new-roman/license.txt +++ b/resources/fonts/code-new-roman/license.txt @@ -38,4 +38,4 @@ TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/resources/images/icons/backspace.svg b/resources/images/icons/backspace.svg index 27e5b8aa..af1122b3 100644 --- a/resources/images/icons/backspace.svg +++ b/resources/images/icons/backspace.svg @@ -37,4 +37,4 @@ - \ No newline at end of file + diff --git a/resources/images/icons/backup.svg b/resources/images/icons/backup.svg index e5c4951e..c573b2e8 100644 --- a/resources/images/icons/backup.svg +++ b/resources/images/icons/backup.svg @@ -86,4 +86,4 @@ - \ No newline at end of file + diff --git a/resources/images/icons/license.svg b/resources/images/icons/license.svg index 7ed4b446..0e086e63 100755 --- a/resources/images/icons/license.svg +++ b/resources/images/icons/license.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/resources/images/icons/moon.svg b/resources/images/icons/moon.svg index a22bc972..bfe4d5c8 100644 --- a/resources/images/icons/moon.svg +++ b/resources/images/icons/moon.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/resources/images/icons/qr.svg b/resources/images/icons/qr.svg index ebb8d1ec..8b5c0b3a 100644 --- a/resources/images/icons/qr.svg +++ b/resources/images/icons/qr.svg @@ -28,4 +28,4 @@ - \ No newline at end of file + diff --git a/resources/images/icons/sun.svg b/resources/images/icons/sun.svg index f31d7354..adae7a68 100644 --- a/resources/images/icons/sun.svg +++ b/resources/images/icons/sun.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/resources/platform/darwin/Info.plist b/resources/platform/darwin/Info.plist index 49ae4dff..e0a1242b 100644 --- a/resources/platform/darwin/Info.plist +++ b/resources/platform/darwin/Info.plist @@ -9,7 +9,7 @@ CFBundleIconFile appIcon.icns CFBundleIdentifier - com.simelo.FiberCryptoWallet + net.fibercrypto.wallet CFBundleName FiberCryptoWallet CFBundlePackageType diff --git a/resources/translations/update_translation_files.sh b/resources/translations/update_translation_files.sh index e10e3a58..302a1128 100644 --- a/resources/translations/update_translation_files.sh +++ b/resources/translations/update_translation_files.sh @@ -4,4 +4,4 @@ lupdate ../../src/ui/*.qml ../../src/ui/Controls/*.qml ../../src/ui/Delegates/*. lupdate ../../src/ui/*.qml ../../src/ui/Controls/*.qml ../../src/ui/Delegates/*.qml ../../src/ui/Dialogs/*.qml -source-language en_us -target-language es_es -ts FiberCryptoWallet_es.ts lupdate ../../src/ui/*.qml ../../src/ui/Controls/*.qml ../../src/ui/Delegates/*.qml ../../src/ui/Dialogs/*.qml -source-language en_us -target-language fr_fr -ts FiberCryptoWallet_fr.ts # echo '\033[1;37mSearching for strings to translate in Go sources...\033[0m' -# find ../../src -path *.go -type f -printf "%p " | lupdate -tr-function-alias translate+=Translate -source-language en_us -target-language en_us -ts FiberCryptoWallet_en.ts \ No newline at end of file +# find ../../src -path *.go -type f -printf "%p " | lupdate -tr-function-alias translate+=Translate -source-language en_us -target-language en_us -ts FiberCryptoWallet_en.ts diff --git a/src/coin/mocks/AltcoinPlugin.go b/src/coin/mocks/AltcoinPlugin.go index 125b2b4d..3c99d3d3 100644 --- a/src/coin/mocks/AltcoinPlugin.go +++ b/src/coin/mocks/AltcoinPlugin.go @@ -93,6 +93,52 @@ func (_m *AltcoinPlugin) LoadPEX(netType string) (core.PEX, error) { return r0, r1 } +// LoadSignService provides a mock function with given fields: +func (_m *AltcoinPlugin) LoadSignService() (core.BlockchainSignService, error) { + ret := _m.Called() + + var r0 core.BlockchainSignService + if rf, ok := ret.Get(0).(func() core.BlockchainSignService); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(core.BlockchainSignService) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LoadTransactionAPI provides a mock function with given fields: netType +func (_m *AltcoinPlugin) LoadTransactionAPI(netType string) (core.BlockchainTransactionAPI, error) { + ret := _m.Called(netType) + + var r0 core.BlockchainTransactionAPI + if rf, ok := ret.Get(0).(func(string) core.BlockchainTransactionAPI); ok { + r0 = rf(netType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(core.BlockchainTransactionAPI) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(netType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // LoadWalletEnvs provides a mock function with given fields: func (_m *AltcoinPlugin) LoadWalletEnvs() []core.WalletEnv { ret := _m.Called() diff --git a/src/coin/mocks/BlockchainSignService.go b/src/coin/mocks/BlockchainSignService.go new file mode 100644 index 00000000..2fda5274 --- /dev/null +++ b/src/coin/mocks/BlockchainSignService.go @@ -0,0 +1,34 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import core "github.com/fibercrypto/fibercryptowallet/src/core" +import mock "github.com/stretchr/testify/mock" + +// BlockchainSignService is an autogenerated mock type for the BlockchainSignService type +type BlockchainSignService struct { + mock.Mock +} + +// Sign provides a mock function with given fields: txn, signSpec, pwd +func (_m *BlockchainSignService) Sign(txn core.Transaction, signSpec []core.InputSignDescriptor, pwd core.PasswordReader) (core.Transaction, error) { + ret := _m.Called(txn, signSpec, pwd) + + var r0 core.Transaction + if rf, ok := ret.Get(0).(func(core.Transaction, []core.InputSignDescriptor, core.PasswordReader) core.Transaction); ok { + r0 = rf(txn, signSpec, pwd) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(core.Transaction) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(core.Transaction, []core.InputSignDescriptor, core.PasswordReader) error); ok { + r1 = rf(txn, signSpec, pwd) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/src/coin/mocks/BlockchainTransactionAPI.go b/src/coin/mocks/BlockchainTransactionAPI.go new file mode 100644 index 00000000..af133a91 --- /dev/null +++ b/src/coin/mocks/BlockchainTransactionAPI.go @@ -0,0 +1,57 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import core "github.com/fibercrypto/fibercryptowallet/src/core" +import mock "github.com/stretchr/testify/mock" + +// BlockchainTransactionAPI is an autogenerated mock type for the BlockchainTransactionAPI type +type BlockchainTransactionAPI struct { + mock.Mock +} + +// SendFromAddress provides a mock function with given fields: from, to, change, options +func (_m *BlockchainTransactionAPI) SendFromAddress(from []core.WalletAddress, to []core.TransactionOutput, change core.Address, options core.KeyValueStore) (core.Transaction, error) { + ret := _m.Called(from, to, change, options) + + var r0 core.Transaction + if rf, ok := ret.Get(0).(func([]core.WalletAddress, []core.TransactionOutput, core.Address, core.KeyValueStore) core.Transaction); ok { + r0 = rf(from, to, change, options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(core.Transaction) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]core.WalletAddress, []core.TransactionOutput, core.Address, core.KeyValueStore) error); ok { + r1 = rf(from, to, change, options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Spend provides a mock function with given fields: unspent, new, change, options +func (_m *BlockchainTransactionAPI) Spend(unspent []core.WalletOutput, new []core.TransactionOutput, change core.Address, options core.KeyValueStore) (core.Transaction, error) { + ret := _m.Called(unspent, new, change, options) + + var r0 core.Transaction + if rf, ok := ret.Get(0).(func([]core.WalletOutput, []core.TransactionOutput, core.Address, core.KeyValueStore) core.Transaction); ok { + r0 = rf(unspent, new, change, options) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(core.Transaction) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]core.WalletOutput, []core.TransactionOutput, core.Address, core.KeyValueStore) error); ok { + r1 = rf(unspent, new, change, options) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/src/coin/mocks/KeyValueStorage.go b/src/coin/mocks/KeyValueStore.go similarity index 67% rename from src/coin/mocks/KeyValueStorage.go rename to src/coin/mocks/KeyValueStore.go index 277b6e1a..4dde59f7 100644 --- a/src/coin/mocks/KeyValueStorage.go +++ b/src/coin/mocks/KeyValueStore.go @@ -4,13 +4,13 @@ package mocks import mock "github.com/stretchr/testify/mock" -// KeyValueStorage is an autogenerated mock type for the KeyValueStorage type -type KeyValueStorage struct { +// KeyValueStore is an autogenerated mock type for the KeyValueStore type +type KeyValueStore struct { mock.Mock } // GetValue provides a mock function with given fields: key -func (_m *KeyValueStorage) GetValue(key string) interface{} { +func (_m *KeyValueStore) GetValue(key string) interface{} { ret := _m.Called(key) var r0 interface{} @@ -26,6 +26,6 @@ func (_m *KeyValueStorage) GetValue(key string) interface{} { } // SetValue provides a mock function with given fields: key, value -func (_m *KeyValueStorage) SetValue(key string, value interface{}) { +func (_m *KeyValueStore) SetValue(key string, value interface{}) { _m.Called(key, value) } diff --git a/src/coin/mocks/Wallet.go b/src/coin/mocks/Wallet.go index 30f5660c..d35a0180 100644 --- a/src/coin/mocks/Wallet.go +++ b/src/coin/mocks/Wallet.go @@ -94,11 +94,11 @@ func (_m *Wallet) GetLoadedAddresses() (core.AddressIterator, error) { } // SendFromAddress provides a mock function with given fields: from, to, change, options -func (_m *Wallet) SendFromAddress(from []core.Address, to []core.TransactionOutput, change core.Address, options core.KeyValueStorage) (core.Transaction, error) { +func (_m *Wallet) SendFromAddress(from []core.Address, to []core.TransactionOutput, change core.Address, options core.KeyValueStore) (core.Transaction, error) { ret := _m.Called(from, to, change, options) var r0 core.Transaction - if rf, ok := ret.Get(0).(func([]core.Address, []core.TransactionOutput, core.Address, core.KeyValueStorage) core.Transaction); ok { + if rf, ok := ret.Get(0).(func([]core.Address, []core.TransactionOutput, core.Address, core.KeyValueStore) core.Transaction); ok { r0 = rf(from, to, change, options) } else { if ret.Get(0) != nil { @@ -107,7 +107,7 @@ func (_m *Wallet) SendFromAddress(from []core.Address, to []core.TransactionOutp } var r1 error - if rf, ok := ret.Get(1).(func([]core.Address, []core.TransactionOutput, core.Address, core.KeyValueStorage) error); ok { + if rf, ok := ret.Get(1).(func([]core.Address, []core.TransactionOutput, core.Address, core.KeyValueStore) error); ok { r1 = rf(from, to, change, options) } else { r1 = ret.Error(1) @@ -145,11 +145,11 @@ func (_m *Wallet) Sign(txn core.Transaction, signer core.TxnSigner, pwd core.Pas } // Spend provides a mock function with given fields: unspent, new, change, options -func (_m *Wallet) Spend(unspent []core.TransactionOutput, new []core.TransactionOutput, change core.Address, options core.KeyValueStorage) (core.Transaction, error) { +func (_m *Wallet) Spend(unspent []core.TransactionOutput, new []core.TransactionOutput, change core.Address, options core.KeyValueStore) (core.Transaction, error) { ret := _m.Called(unspent, new, change, options) var r0 core.Transaction - if rf, ok := ret.Get(0).(func([]core.TransactionOutput, []core.TransactionOutput, core.Address, core.KeyValueStorage) core.Transaction); ok { + if rf, ok := ret.Get(0).(func([]core.TransactionOutput, []core.TransactionOutput, core.Address, core.KeyValueStore) core.Transaction); ok { r0 = rf(unspent, new, change, options) } else { if ret.Get(0) != nil { @@ -158,7 +158,7 @@ func (_m *Wallet) Spend(unspent []core.TransactionOutput, new []core.Transaction } var r1 error - if rf, ok := ret.Get(1).(func([]core.TransactionOutput, []core.TransactionOutput, core.Address, core.KeyValueStorage) error); ok { + if rf, ok := ret.Get(1).(func([]core.TransactionOutput, []core.TransactionOutput, core.Address, core.KeyValueStore) error); ok { r1 = rf(unspent, new, change, options) } else { r1 = ret.Error(1) @@ -168,11 +168,11 @@ func (_m *Wallet) Spend(unspent []core.TransactionOutput, new []core.Transaction } // Transfer provides a mock function with given fields: to, options -func (_m *Wallet) Transfer(to core.TransactionOutput, options core.KeyValueStorage) (core.Transaction, error) { +func (_m *Wallet) Transfer(to core.TransactionOutput, options core.KeyValueStore) (core.Transaction, error) { ret := _m.Called(to, options) var r0 core.Transaction - if rf, ok := ret.Get(0).(func(core.TransactionOutput, core.KeyValueStorage) core.Transaction); ok { + if rf, ok := ret.Get(0).(func(core.TransactionOutput, core.KeyValueStore) core.Transaction); ok { r0 = rf(to, options) } else { if ret.Get(0) != nil { @@ -181,7 +181,7 @@ func (_m *Wallet) Transfer(to core.TransactionOutput, options core.KeyValueStora } var r1 error - if rf, ok := ret.Get(1).(func(core.TransactionOutput, core.KeyValueStorage) error); ok { + if rf, ok := ret.Get(1).(func(core.TransactionOutput, core.KeyValueStore) error); ok { r1 = rf(to, options) } else { r1 = ret.Error(1) diff --git a/src/coin/mocks/WalletAddress.go b/src/coin/mocks/WalletAddress.go new file mode 100644 index 00000000..35a4fd3e --- /dev/null +++ b/src/coin/mocks/WalletAddress.go @@ -0,0 +1,43 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import core "github.com/fibercrypto/fibercryptowallet/src/core" +import mock "github.com/stretchr/testify/mock" + +// WalletAddress is an autogenerated mock type for the WalletAddress type +type WalletAddress struct { + mock.Mock +} + +// GetAddress provides a mock function with given fields: +func (_m *WalletAddress) GetAddress() core.Address { + ret := _m.Called() + + var r0 core.Address + if rf, ok := ret.Get(0).(func() core.Address); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(core.Address) + } + } + + return r0 +} + +// GetWallet provides a mock function with given fields: +func (_m *WalletAddress) GetWallet() core.Wallet { + ret := _m.Called() + + var r0 core.Wallet + if rf, ok := ret.Get(0).(func() core.Wallet); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(core.Wallet) + } + } + + return r0 +} diff --git a/src/coin/mocks/WalletOutput.go b/src/coin/mocks/WalletOutput.go new file mode 100644 index 00000000..58ac6516 --- /dev/null +++ b/src/coin/mocks/WalletOutput.go @@ -0,0 +1,43 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import core "github.com/fibercrypto/fibercryptowallet/src/core" +import mock "github.com/stretchr/testify/mock" + +// WalletOutput is an autogenerated mock type for the WalletOutput type +type WalletOutput struct { + mock.Mock +} + +// GetOutput provides a mock function with given fields: +func (_m *WalletOutput) GetOutput() core.TransactionOutput { + ret := _m.Called() + + var r0 core.TransactionOutput + if rf, ok := ret.Get(0).(func() core.TransactionOutput); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(core.TransactionOutput) + } + } + + return r0 +} + +// GetWallet provides a mock function with given fields: +func (_m *WalletOutput) GetWallet() core.Wallet { + ret := _m.Called() + + var r0 core.Wallet + if rf, ok := ret.Get(0).(func() core.Wallet); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(core.Wallet) + } + } + + return r0 +} diff --git a/src/coin/skycoin/models/blockchain.go b/src/coin/skycoin/models/blockchain.go index 6f897be3..5cc9e71e 100644 --- a/src/coin/skycoin/models/blockchain.go +++ b/src/coin/skycoin/models/blockchain.go @@ -84,17 +84,17 @@ type SkycoinBlockchainInfo struct { NumberOfBlocks *readable.BlockchainProgress } -type SkycoinBlockchainStatus struct { //Implements BlockchainStatus interface +type SkycoinBlockchain struct { //Implements BlockchainStatus interface lastTimeStatusRequested uint64 //nolint structcheck TODO: Not used lastTimeSupplyRequested uint64 CacheTime uint64 cachedStatus *SkycoinBlockchainInfo } -func NewSkycoinBlockchainStatus(invalidCacheTime uint64) *SkycoinBlockchainStatus { - return &SkycoinBlockchainStatus{CacheTime: invalidCacheTime} +func NewSkycoinBlockchain(invalidCacheTime uint64) *SkycoinBlockchain { + return &SkycoinBlockchain{CacheTime: invalidCacheTime} } -func (ss *SkycoinBlockchainStatus) GetCoinValue(coinvalue core.CoinValueMetric, ticker string) (uint64, error) { +func (ss *SkycoinBlockchain) GetCoinValue(coinvalue core.CoinValueMetric, ticker string) (uint64, error) { logBlockchain.Info("Getting Coin value") elapsed := uint64(time.Now().UTC().UnixNano()) - ss.lastTimeSupplyRequested if elapsed > ss.CacheTime || ss.cachedStatus == nil { @@ -122,7 +122,7 @@ func (ss *SkycoinBlockchainStatus) GetCoinValue(coinvalue core.CoinValueMetric, } } -func (ss *SkycoinBlockchainStatus) GetLastBlock() (core.Block, error) { +func (ss *SkycoinBlockchain) GetLastBlock() (core.Block, error) { logBlockchain.Info("Getting last block") elapsed := uint64(time.Now().UTC().UnixNano()) - ss.lastTimeSupplyRequested if elapsed > ss.CacheTime || ss.cachedStatus == nil { @@ -136,7 +136,7 @@ func (ss *SkycoinBlockchainStatus) GetLastBlock() (core.Block, error) { return ss.cachedStatus.LastBlockInfo, nil } -func (ss *SkycoinBlockchainStatus) GetNumberOfBlocks() (uint64, error) { +func (ss *SkycoinBlockchain) GetNumberOfBlocks() (uint64, error) { logBlockchain.Info("Getting number of blocks") if ss.cachedStatus == nil { if ss.cachedStatus == nil { @@ -151,12 +151,12 @@ func (ss *SkycoinBlockchainStatus) GetNumberOfBlocks() (uint64, error) { return ss.cachedStatus.NumberOfBlocks.Current, nil } -func (ss *SkycoinBlockchainStatus) SetCacheTime(time uint64) { +func (ss *SkycoinBlockchain) SetCacheTime(time uint64) { logBlockchain.Info("Setting cache time") ss.CacheTime = time } -func (ss *SkycoinBlockchainStatus) requestSupplyInfo() error { +func (ss *SkycoinBlockchain) requestSupplyInfo() error { logBlockchain.Info("Requesting supply info") c, err := NewSkycoinApiClient(PoolSection) @@ -207,7 +207,7 @@ func (ss *SkycoinBlockchainStatus) requestSupplyInfo() error { return nil } -func (ss *SkycoinBlockchainStatus) requestStatusInfo() error { +func (ss *SkycoinBlockchain) requestStatusInfo() error { logBlockchain.Info("Requesting status information") c, err := NewSkycoinApiClient(PoolSection) if err != nil { @@ -238,3 +238,26 @@ func (ss *SkycoinBlockchainStatus) requestStatusInfo() error { return nil } + +// SendFromAddress instantiates a transaction to send funds from specific source addresses +// to multiple destination addresses +func (ss *SkycoinBlockchain) SendFromAddress(from []core.WalletAddress, to []core.TransactionOutput, change core.Address, options core.KeyValueStore) (core.Transaction, error) { + logBlockchain.Info("Sending coins from addresses via blockchain API") + addresses := make([]core.Address, len(from)) + for i, wa := range from { + addresses[i] = wa.GetAddress() + } + createTxnFunc := skyAPICreateTxn + return createTransaction(addresses, to, nil, change, options, createTxnFunc) +} + +// Spend instantiates a transaction that spends specific outputs to send to multiple destination addresses +func (ss *SkycoinBlockchain) Spend(unspent []core.WalletOutput, new []core.TransactionOutput, change core.Address, options core.KeyValueStore) (core.Transaction, error) { + logBlockchain.Info("Spending coins from outputs via blockchain API") + uxouts := make([]core.TransactionOutput, len(unspent)) + for i, wu := range unspent { + uxouts[i] = wu.GetOutput() + } + createTxnFunc := skyAPICreateTxn + return createTransaction(nil, new, uxouts, change, options, createTxnFunc) +} diff --git a/src/coin/skycoin/models/blockchain_test.go b/src/coin/skycoin/models/blockchain_test.go index 86fe59b7..c03f5eef 100644 --- a/src/coin/skycoin/models/blockchain_test.go +++ b/src/coin/skycoin/models/blockchain_test.go @@ -22,7 +22,7 @@ func TestSkycoinBlockchainStatusGetCoinValue(t *testing.T) { ) global_mock.On("BlockchainProgress").Return(&readable.BlockchainProgress{}, nil) - block := &SkycoinBlockchainStatus{CacheTime: 20} + block := &SkycoinBlockchain{CacheTime: 20} val, err := block.GetCoinValue(core.CoinCurrentSupply, Sky) require.NoError(t, err) require.Equal(t, val, uint64(200111111)) @@ -56,7 +56,7 @@ func TestSkycoinBlockchainStatusGetNumberOfBlocks(t *testing.T) { nil, ) - block := &SkycoinBlockchainStatus{CacheTime: 20} + block := &SkycoinBlockchain{CacheTime: 20} val, err := block.GetNumberOfBlocks() require.NoError(t, err) require.Equal(t, val, uint64(20)) @@ -76,7 +76,7 @@ func TestSkycoinBlockchainStatusGetLastBlock(t *testing.T) { ) global_mock.On("BlockchainProgress").Return(&readable.BlockchainProgress{}, nil) - status := &SkycoinBlockchainStatus{CacheTime: 20} + status := &SkycoinBlockchain{CacheTime: 20} block, err := status.GetLastBlock() require.NoError(t, err) val, err2 := block.GetVersion() diff --git a/src/coin/skycoin/models/main.go b/src/coin/skycoin/models/main.go index 28b698b2..565a71e8 100644 --- a/src/coin/skycoin/models/main.go +++ b/src/coin/skycoin/models/main.go @@ -5,12 +5,15 @@ import ( "github.com/fibercrypto/fibercryptowallet/src/core" "github.com/fibercrypto/fibercryptowallet/src/errors" local "github.com/fibercrypto/fibercryptowallet/src/main" + appParams "github.com/fibercrypto/fibercryptowallet/src/params" ) +// SkyFiberPlugin provide support for SkyFiber coins type SkyFiberPlugin struct { Params params.SkyFiberParams } +// ListSupportedAltcoins to enumerate supported assets and related metadata func (p *SkyFiberPlugin) ListSupportedAltcoins() []core.AltcoinMetadata { return []core.AltcoinMetadata{ core.AltcoinMetadata{ @@ -23,38 +26,43 @@ func (p *SkyFiberPlugin) ListSupportedAltcoins() []core.AltcoinMetadata { core.AltcoinMetadata{ Name: CoinHoursName, Ticker: CoinHoursTicker, - Family: CoinHoursFamily, + Family: SkycoinFamily, HasBip44: false, Accuracy: 0, }, core.AltcoinMetadata{ Name: CalculatedHoursName, Ticker: CalculatedHoursTicker, - Family: CalculatedHoursFamily, + Family: SkycoinFamily, HasBip44: false, Accuracy: 0, }, } } +// ListSupportedFamilies classifies similar cryptocurrencies into a family func (p *SkyFiberPlugin) ListSupportedFamilies() []string { return []string{SkycoinFamily} } +// RegisterTo boilerplate to register this plugin against an altcoin manager and enable it func (p *SkyFiberPlugin) RegisterTo(manager core.AltcoinManager) { for _, info := range p.ListSupportedAltcoins() { manager.RegisterAltcoin(info, p) } } +// GetName provides concise human-readable caption o identify this plugin func (p *SkyFiberPlugin) GetName() string { return "SkyFiber" } +// GetDescription describes plugin and its features func (p *SkyFiberPlugin) GetDescription() string { return "FiberCrypto wallet connector for Skycoin and SkyFiber altcoins" } +// LoadWalletEnvs loads wallet environments to lookup and create wallets func (p *SkyFiberPlugin) LoadWalletEnvs() []core.WalletEnv { config := local.GetConfigManager() @@ -76,15 +84,25 @@ func (p *SkyFiberPlugin) LoadWalletEnvs() []core.WalletEnv { return wltEnvs } +// LoadPEX instantiates proxy object to interact with nodes nodes of the P2P network func (p *SkyFiberPlugin) LoadPEX(netType string) (core.PEX, error) { - var poolSection string - if netType == "MainNet" { - poolSection = PoolSection - } else { + if netType != "MainNet" { return nil, errors.ErrInvalidNetworkType } - return NewSkycoinPEX(poolSection), nil + return NewSkycoinPEX(PoolSection), nil +} +// LoadTransactionAPI blockchain transaction API entry poiny +func (p *SkyFiberPlugin) LoadTransactionAPI(netType string) (core.BlockchainTransactionAPI, error) { + if netType != "MainNet" { + return nil, errors.ErrInvalidNetworkType + } + return NewSkycoinBlockchain(appParams.DataRefreshTimeout), nil +} + +// LoadSignService sign service entry point +func (p *SkyFiberPlugin) LoadSignService() (core.BlockchainSignService, error) { + return &SkycoinSignService{}, nil } func NewSkyFiberPlugin(params params.SkyFiberParams) core.AltcoinPlugin { @@ -92,3 +110,8 @@ func NewSkyFiberPlugin(params params.SkyFiberParams) core.AltcoinPlugin { Params: params, } } + +// Type assertions +var ( + _ core.AltcoinPlugin = &SkyFiberPlugin{} +) diff --git a/src/coin/skycoin/models/params.go b/src/coin/skycoin/models/params.go index 3983da34..b6fc1886 100644 --- a/src/coin/skycoin/models/params.go +++ b/src/coin/skycoin/models/params.go @@ -18,10 +18,8 @@ const ( SkycoinDescription = params.SkycoinDescription CoinHoursTicker = params.CoinHoursTicker CoinHoursName = params.CoinHoursName - CoinHoursFamily = params.CoinHoursFamily CoinHoursDescription = params.CoinHoursDescription CalculatedHoursTicker = params.CalculatedHoursTicker CalculatedHoursName = params.CalculatedHoursName - CalculatedHoursFamily = params.CalculatedHoursFamily CalculatedHoursDescription = params.CalculatedHoursDescription ) diff --git a/src/coin/skycoin/models/sign.go b/src/coin/skycoin/models/sign.go new file mode 100644 index 00000000..986772e3 --- /dev/null +++ b/src/coin/skycoin/models/sign.go @@ -0,0 +1,19 @@ +package skycoin + +import ( + "github.com/fibercrypto/fibercryptowallet/src/core" + "github.com/fibercrypto/fibercryptowallet/src/util" +) + +// SkycoinSignService implements BlockchainSignService for multi-wallet transaction signing +type SkycoinSignService struct{} + +// Sign creates a new transaction by (fully or partially) signing a given transaction +func (sss *SkycoinSignService) Sign(txn core.Transaction, signSpec []core.InputSignDescriptor, pwd core.PasswordReader) (core.Transaction, error) { + return util.GenericMultiWalletSign(txn, signSpec, pwd) +} + +// Type assertions +var ( + _ core.BlockchainSignService = &SkycoinSignService{} +) diff --git a/src/coin/skycoin/models/sky_test.go b/src/coin/skycoin/models/sky_test.go index e6ac6a17..f35a2989 100644 --- a/src/coin/skycoin/models/sky_test.go +++ b/src/coin/skycoin/models/sky_test.go @@ -1,5 +1,12 @@ package skycoin +/** + * This file contains test code copied from github.com/skycoin/skycoin codebase + * for it is needed by test suite and functions not exported in upstream package. + * + * Please only add in here code copied from Skycoin codebase. Make sure it's strictly necessary. + */ + import ( "crypto/rand" "path/filepath" diff --git a/src/coin/skycoin/models/testdata/test.wlt b/src/coin/skycoin/models/testdata/test.wlt index bb0512de..d782322c 100644 --- a/src/coin/skycoin/models/testdata/test.wlt +++ b/src/coin/skycoin/models/testdata/test.wlt @@ -36,4 +36,4 @@ "secret_key": "7418dd56fc6fef3943673c534bc4d21bfb00f18bf4ffacf8c6e06489299ca26a" } ] -} \ No newline at end of file +} diff --git a/src/coin/skycoin/models/util_test.go b/src/coin/skycoin/models/util_test.go new file mode 100644 index 00000000..4e6ab17d --- /dev/null +++ b/src/coin/skycoin/models/util_test.go @@ -0,0 +1,84 @@ +package skycoin + +import ( + "testing" + + "github.com/skycoin/skycoin/src/cipher/bip39" + + "github.com/fibercrypto/fibercryptowallet/src/core" + "github.com/fibercrypto/fibercryptowallet/src/util" + "github.com/skycoin/skycoin/src/cipher" + "github.com/skycoin/skycoin/src/coin" + "github.com/skycoin/skycoin/src/testutil" + "github.com/stretchr/testify/require" +) + +func generateRandomKeyData(t *testing.T) *KeyData { + entropy, err := bip39.NewEntropy(128) + require.NoError(t, err) + mnemonic, err := bip39.NewMnemonic(entropy) + require.NoError(t, err) + require.NoError(t, err) + pubKey, secKey, err := cipher.GenerateDeterministicKeyPair([]byte(mnemonic)) + require.NoError(t, err) + + kd := &KeyData{ + AddressIndex: 0, + Entropy: entropy, + Mnemonic: mnemonic, + PubKey: pubKey, + SecKey: secKey, + } + + return kd +} + +func makeUxBodyWithRandomSecret(t *testing.T) (coin.UxBody, *KeyData) { + keydata := generateRandomKeyData(t) + return coin.UxBody{ + SrcTransaction: testutil.RandSHA256(t), + Address: cipher.AddressFromPubKey(keydata.PubKey), + Coins: 1e6, + Hours: 100, + }, keydata +} + +func makeUxWithRandomSecret(t *testing.T) (coin.UxOut, *KeyData) { + body, kd := makeUxBodyWithRandomSecret(t) + return coin.UxOut{ + Head: coin.UxHead{ + Time: 100, + BkSeq: 2, + }, + Body: body, + }, kd +} + +func makeTransactionFromMultipleWallets(t *testing.T, n int) (coin.Transaction, []KeyData, []coin.UxOut) { + uxs := make([]coin.UxOut, n) + keysdata := make([]KeyData, n) + secs := make([]cipher.SecKey, n) + + for i := 0; i < n; i++ { + ux, kd := makeUxWithRandomSecret(t) + uxs[i] = ux + secs[i] = kd.SecKey + keysdata[i] = *kd + } + + return makeTransactionFromUxOuts(t, uxs, secs), keysdata, uxs +} + +func makeSimpleWalletAddress(wallet core.Wallet, address core.Address) core.WalletAddress { + return &util.SimpleWalletAddress{ + Wallet: wallet, + UxOut: address, + } +} + +func makeSimpleWalletOutput(wallet core.Wallet, out core.TransactionOutput) core.WalletOutput { + return &util.SimpleWalletOutput{ + Wallet: wallet, + UxOut: out, + } +} diff --git a/src/coin/skycoin/models/wallet.go b/src/coin/skycoin/models/wallet.go index 5ea5606a..c4e23852 100644 --- a/src/coin/skycoin/models/wallet.go +++ b/src/coin/skycoin/models/wallet.go @@ -20,6 +20,7 @@ import ( "github.com/skycoin/skycoin/src/cipher/bip39" "github.com/skycoin/skycoin/src/coin" "github.com/skycoin/skycoin/src/readable" + "github.com/skycoin/skycoin/src/visor" "github.com/skycoin/skycoin/src/wallet" ) @@ -30,6 +31,7 @@ const ( CoinHour = params.CoinHoursTicker CalculatedHour = params.CalculatedHoursTicker + walletExt = ".wlt" WalletTimestampFormat = "2006_01_02" @@ -37,7 +39,7 @@ const ( SignerIDRemoteWallet = "sky.remote" ) -//Implements WalletIterator interface +// SkycoinWalletIterator implements WalletIterator interface type SkycoinWalletIterator struct { current int wallets []core.Wallet @@ -105,7 +107,11 @@ func (wltSrv *SkycoinRemoteWallet) CreateWallet(label string, seed string, wltTy } defer ReturnSkycoinClient(c) if IsEncrypted { - password, err := pwd("Enter your password") + pwdCtx := util.NewKeyValueMap() + pwdCtx.SetValue(core.StrTypeName, core.TypeNameWalletSet) + pwdCtx.SetValue(core.StrMethodName, "CreateWallet") + pwdCtx.SetValue(core.StrWalletLabel, label) + password, err := pwd("Enter password to encrypt wallet", pwdCtx) if err != nil { logWallet.WithError(err).Fatal("Something was wrong entering the password") return nil, err @@ -166,7 +172,11 @@ func (wltSrv *SkycoinRemoteWallet) Encrypt(walletName string, pwd core.PasswordR return } defer ReturnSkycoinClient(c) - password, err := pwd("Insert password") + pwdCtx := util.NewKeyValueMap() + pwdCtx.SetValue(core.StrTypeName, core.TypeNameWalletStorage) + pwdCtx.SetValue(core.StrMethodName, "Encrypt") + pwdCtx.SetValue(core.StrWalletName, walletName) + password, err := pwd("Enter password to encrypt wallet", pwdCtx) if err != nil { logWallet.WithError(err).Fatal("Something was wrong entering the password") return @@ -187,7 +197,11 @@ func (wltSrv *SkycoinRemoteWallet) Decrypt(walletName string, pwd core.PasswordR return } defer ReturnSkycoinClient(c) - password, err := pwd("Insert password") + pwdCtx := util.NewKeyValueMap() + pwdCtx.SetValue(core.StrTypeName, core.TypeNameWalletStorage) + pwdCtx.SetValue(core.StrMethodName, "Decrypt") + pwdCtx.SetValue(core.StrWalletName, walletName) + password, err := pwd("Enter password to decrypt wallet", pwdCtx) if err != nil { logWallet.WithError(err).Fatal("Something was wrong entering the password") return @@ -363,6 +377,7 @@ func (wlt *RemoteWallet) Sign(txn core.Transaction, signer core.TxnSigner, pwd c func (wlt *RemoteWallet) signSkycoinTxn(txn core.Transaction, pwd core.PasswordReader, index []int) (core.Transaction, error) { client, err := NewSkycoinApiClient(PoolSection) + var password string = "" if err != nil { logWallet.WithError(err).Warn(err) return nil, err @@ -373,11 +388,19 @@ func (wlt *RemoteWallet) signSkycoinTxn(txn core.Transaction, pwd core.PasswordR logWallet.WithError(err).Warn(err) return nil, errors.ErrInvalidTxn } - password, err := pwd(fmt.Sprintf("Enter password to decrypt wallet '%s'", wlt.Id)) - if err != nil { - logWallet.WithError(err).Warn("Error getting password") - return nil, err + if wlt.Encrypted { + pwdCtx := util.NewKeyValueMap() + pwdCtx.SetValue(core.StrTypeName, core.TypeNameWallet) + pwdCtx.SetValue(core.StrMethodName, "Sign") + pwdCtx.SetValue(core.StrWalletName, wlt.Id) + pwdCtx.SetValue(core.StrWalletLabel, wlt.Label) + password, err = pwd("Enter password", pwdCtx) + if err != nil { + logWallet.WithError(err).Warn("Error getting password") + return nil, err + } } + txnBytes, err := skyTxn.EncodeSkycoinTransaction() if err != nil { logWallet.WithError(err).Warn("Couldn't get Transaction Encoded") @@ -424,7 +447,7 @@ func (wlt *RemoteWallet) GetId() string { return wlt.Id } -func (wlt *RemoteWallet) Transfer(destination core.TransactionOutput, options core.KeyValueStorage) (core.Transaction, error) { +func (wlt *RemoteWallet) Transfer(destination core.TransactionOutput, options core.KeyValueStore) (core.Transaction, error) { logWallet.Info("Transfer from remote wallet") amount, err := destination.GetCoins(SkycoinTicker) if err != nil { @@ -468,7 +491,7 @@ func (wlt *RemoteWallet) Transfer(destination core.TransactionOutput, options co type createTxn func(*api.CreateTransactionRequest) (core.Transaction, error) -func createTransaction(from []core.Address, to, uxOut []core.TransactionOutput, change core.Address, options core.KeyValueStorage, createTxnFunc createTxn) (core.Transaction, error) { +func createTransaction(from []core.Address, to, uxOut []core.TransactionOutput, change core.Address, options core.KeyValueStore, createTxnFunc createTxn) (core.Transaction, error) { logWallet.Info("Creating transaction...") var req api.CreateTransactionRequest req.IgnoreUnconfirmed = false @@ -484,7 +507,7 @@ func createTransaction(from []core.Address, to, uxOut []core.TransactionOutput, if uxOut != nil { uxOuts := make([]string, 0) for _, out := range uxOut { - uxOuts = append(uxOuts, out.GetAddress().String()) + uxOuts = append(uxOuts, out.GetId()) } req.UxOuts = uxOuts } @@ -557,7 +580,7 @@ func createTransaction(from []core.Address, to, uxOut []core.TransactionOutput, } -func (wlt *RemoteWallet) SendFromAddress(from []core.Address, to []core.TransactionOutput, change core.Address, options core.KeyValueStorage) (core.Transaction, error) { +func (wlt *RemoteWallet) SendFromAddress(from []core.Address, to []core.TransactionOutput, change core.Address, options core.KeyValueStore) (core.Transaction, error) { logWallet.Info("Sending from address of remote wallets") createTxnFunc := func(txnR *api.CreateTransactionRequest) (core.Transaction, error) { logWallet.Info("Creating transaction for remote wallet") @@ -584,7 +607,7 @@ func (wlt *RemoteWallet) SendFromAddress(from []core.Address, to []core.Transact return createTransaction(from, to, nil, change, options, createTxnFunc) } -func (wlt *RemoteWallet) Spend(unspent, new []core.TransactionOutput, change core.Address, options core.KeyValueStorage) (core.Transaction, error) { +func (wlt *RemoteWallet) Spend(unspent, new []core.TransactionOutput, change core.Address, options core.KeyValueStore) (core.Transaction, error) { createTxnFunc := func(txnR *api.CreateTransactionRequest) (core.Transaction, error) { logWallet.Info("Spend using remote wallet") var req api.WalletCreateTransactionRequest @@ -618,7 +641,12 @@ func (wlt *RemoteWallet) GenAddresses(addrType core.AddressType, startIndex, cou return nil } defer ReturnSkycoinClient(c) - password, err := pwd("Insert password") + pwdCtx := util.NewKeyValueMap() + pwdCtx.SetValue(core.StrTypeName, core.TypeNameWallet) + pwdCtx.SetValue(core.StrMethodName, "GenAddresses") + pwdCtx.SetValue(core.StrWalletName, wlt.Id) + pwdCtx.SetValue(core.StrWalletLabel, wlt.Label) + password, err := pwd("Enter password", pwdCtx) if err != nil { logWallet.WithError(err).Fatal("Something was wrong entering the password") return nil @@ -693,16 +721,50 @@ func (wlt *RemoteWallet) SignTransaction(txn core.Transaction, pwdReader core.Pa if strIdxs == nil { indices = nil } else { - indices = make([]int, len(strIdxs)) - for i, strIdx := range strIdxs { - indices[i], err = strconv.Atoi(strIdx) + indices, err = getHashIndices(txn.GetInputs(), strIdxs) + if err != nil { + logWallet.Error("Error parsing Skycoin transaction input indices array for signing") + return nil, err + } + } + signedTxn, err = wlt.signSkycoinTxn(txn, pwdReader, indices) + return +} + +func getHashIndices(ins []core.TransactionInput, strIdxs []string) (indices []int, err error) { + cache := make(map[string]int, len(ins)) + indices = make([]int, len(strIdxs)) + scanIdx := 0 + for i, strIdx := range strIdxs { + if strIdx[0] == '#' { + // Parse index + index, err := strconv.Atoi(strIdx[1:]) if err != nil { return nil, errors.ErrIntegerInputsRequired } + indices[i] = index + } else if index, isCached := cache[strIdx]; isCached { + // Found in previous scan + indices[i] = index + } else { + logWallet.Infof("Scanning inputs array looking for %s", strIdx) + // Continue scanning for UXID position in slice + notfound := true + for ; scanIdx < len(ins) && notfound; scanIdx++ { + uxID := ins[scanIdx].GetId() + logWallet.Infof("Scanning inputs array found %s", uxID) + cache[uxID] = scanIdx + if uxID == strIdx { + indices[i] = scanIdx + notfound = false + } + } + if notfound { + return nil, errors.ErrNotFound + } } } - signedTxn, err = wlt.signSkycoinTxn(txn, pwdReader, indices) - return + return indices, nil } func (wlt *RemoteWallet) GetSignerUID() core.UID { @@ -817,7 +879,11 @@ func (wltSrv *SkycoinLocalWallet) GetWallet(id string) core.Wallet { func (wltSrv *SkycoinLocalWallet) CreateWallet(label string, seed string, wltType string, IsEncrypted bool, pwd core.PasswordReader, scanAddressesN int) (core.Wallet, error) { logWallet.Info("Creating Skycoin local wallet") - password, err := pwd("Insert Password") + pwdCtx := util.NewKeyValueMap() + pwdCtx.SetValue(core.StrTypeName, core.TypeNameWalletSet) + pwdCtx.SetValue(core.StrMethodName, "CreateWallet") + pwdCtx.SetValue(core.StrWalletLabel, label) + password, err := pwd("Insert Password", pwdCtx) if err != nil { logWallet.WithError(err).Fatal("Something was wrong entering the password") @@ -901,11 +967,17 @@ func (wltSrv *SkycoinLocalWallet) Encrypt(walletName string, password core.Passw return } + wltLabel := wlt.Label() if wlt.IsEncrypted() { return } - pwd, err := password("Insert Password") + pwdCtx := util.NewKeyValueMap() + pwdCtx.SetValue(core.StrTypeName, core.TypeNameWalletStorage) + pwdCtx.SetValue(core.StrMethodName, "Encrypt") + pwdCtx.SetValue(core.StrWalletName, wltName) + pwdCtx.SetValue(core.StrWalletLabel, wltLabel) + pwd, err := password("Enter Password", pwdCtx) if err != nil { logWallet.WithError(err).Fatal("Something was wrong entering the password") return @@ -935,7 +1007,13 @@ func (wltSrv *SkycoinLocalWallet) Decrypt(walletName string, password core.Passw if !wlt.IsEncrypted() { return } - pwd, err := password("Insert Password") + wltLabel := wlt.Label() + pwdCtx := util.NewKeyValueMap() + pwdCtx.SetValue(core.StrTypeName, core.TypeNameWalletStorage) + pwdCtx.SetValue(core.StrMethodName, "Decrypt") + pwdCtx.SetValue(core.StrWalletName, wltName) + pwdCtx.SetValue(core.StrWalletLabel, wltLabel) + pwd, err := password("Enter Password", pwdCtx) if err != nil { logWallet.WithError(err).Fatal("Something was wrong entering the password") return @@ -1041,22 +1119,33 @@ func (wlt *LocalWallet) signSkycoinTxn(txn core.Transaction, pwd core.PasswordRe var err error var uxouts []coin.UxOut var txnFee uint64 - + var resultTxn core.Transaction walletDir := filepath.Join(wlt.WalletDir, wlt.Id) skyWlt, err := wallet.Load(walletDir) + var originalInputs []api.CreatedTransactionInput + if err != nil { logWallet.WithError(err).Warn("Couldn't load api client") return nil, err } - if rTxn, isReadableTxn := txn.(skytypes.ReadableTxn); isReadableTxn { + rTxn, isReadableTxn := txn.(skytypes.ReadableTxn) + if isReadableTxn { // Readable tranasctions should not need extra API calls + cTxn, err := rTxn.ToCreatedTransaction() if err != nil { + logWallet.WithError(err).Warn("Failed to convert to readable transaction") return nil, err } + originalInputs = cTxn.In if skyWlt.IsEncrypted() { - pass, err := pwd("Type your password") + pwdCtx := util.NewKeyValueMap() + pwdCtx.SetValue(core.StrTypeName, core.TypeNameWallet) + pwdCtx.SetValue(core.StrMethodName, "Sign") + pwdCtx.SetValue(core.StrWalletName, wlt.Id) + pwdCtx.SetValue(core.StrWalletLabel, wlt.Label) + pass, err := pwd("Enter password", pwdCtx) if err != nil { logWallet.WithError(err).Warn("Couldn't get password") return nil, err @@ -1119,6 +1208,7 @@ func (wlt *LocalWallet) signSkycoinTxn(txn core.Transaction, pwd core.PasswordRe } } else { // Raw transaction + unTxn, ok := txn.(*SkycoinUninjectedTransaction) if !ok { logWallet.WithError(err).Warn("Couldn't load transaction un injected") @@ -1137,7 +1227,11 @@ func (wlt *LocalWallet) signSkycoinTxn(txn core.Transaction, pwd core.PasswordRe if skyWlt.IsEncrypted() { - pass, err := pwd("Type your password") + pwdCtx := util.NewKeyValueMap() + pwdCtx.SetValue(core.StrTypeName, core.TypeNameWallet) + pwdCtx.SetValue(core.StrMethodName, "Sign") + pwdCtx.SetValue(core.StrWalletName, wlt.Id) + pass, err := pwd("Enter password", pwdCtx) if err != nil { logWallet.WithError(err).Warn("Couldn't get password") return nil, err @@ -1153,9 +1247,11 @@ func (wlt *LocalWallet) signSkycoinTxn(txn core.Transaction, pwd core.PasswordRe uxouts = make([]coin.UxOut, 0) for _, in := range unTxn.txn.In { ux, err := clt.UxOut(in.String()) + if err != nil { return nil, err } + addr, err := cipher.DecodeBase58Address(ux.OwnerAddress) if err != nil { return nil, err @@ -1183,17 +1279,41 @@ func (wlt *LocalWallet) signSkycoinTxn(txn core.Transaction, pwd core.PasswordRe if len(skyTxn.Sigs) == 0 { skyTxn.Sigs = make([]cipher.Sig, len(skyTxn.In)) } + signedTxn, err := wallet.SignTransaction(skyWlt, skyTxn, index, uxouts) + if err != nil { logWallet.WithError(err).Warn("Couldn't sign transaction using local wallet") return nil, err } - // FIXME: Return readable SkycoinCreatedTransaction since UX data is available - resultTxn, err := NewUninjectedTransaction(signedTxn, txnFee) - if err != nil { - logWallet.WithError(err).Warn("Couldn't create an un injected transaction") - return nil, err + + if isReadableTxn { + vins := make([]visor.TransactionInput, 0) + for _, ux := range uxouts { + vin, err := visor.NewTransactionInput(ux, 0) + if err != nil { + logWallet.WithError(err).Warn("Couldn't create a transaction input") + return nil, err + } + vins = append(vins, vin) + } + + crtTxn, err := api.NewCreatedTransaction(signedTxn, vins) + crtTxn.In = originalInputs + + if err != nil { + logWallet.WithError(err).Warn("Couldn't create an un SkycoinCreatedTransaction") + return nil, err + } + + resultTxn = NewSkycoinCreatedTransaction(*crtTxn) + } else { + resultTxn, err = NewUninjectedTransaction(signedTxn, txnFee) + if err != nil { + return nil, err + } } + return resultTxn, nil } @@ -1230,24 +1350,37 @@ func fromTxnResponse(txnResponse *api.CreateTransactionResponse) *SkycoinCreated return NewSkycoinCreatedTransaction(txnResponse.Transaction) } -func (wlt *LocalWallet) Transfer(destination core.TransactionOutput, options core.KeyValueStorage) (core.Transaction, error) { - logWallet.Info("Transfer from local wallet") - amount, err := destination.GetCoins(SkycoinTicker) +func skyAPICreateTxn(txnReq *api.CreateTransactionRequest) (core.Transaction, error) { + client, err := NewSkycoinApiClient(PoolSection) if err != nil { - logWallet.WithError(err).Warnf("Couldn't retrieve %s to transfer", params.SkycoinTicker) + logWallet.WithError(err).Warn("Couldn't load api client") return nil, err } - to := destination.GetAddress() + defer ReturnSkycoinClient(client) + txnR, err := client.CreateTransaction(*txnReq) + if err != nil { + logWallet.WithError(err).Warn("Couldn't create transaction") + return nil, err + } + return fromTxnResponse(txnR), nil +} +func (wlt *LocalWallet) Transfer(to core.TransactionOutput, options core.KeyValueStore) (core.Transaction, error) { + logWallet.Info("Sending form local wallet") quotient, err := util.AltcoinQuotient(Sky) if err != nil { logWallet.WithError(err).Warn("Couldn't get skycoin quotient") return nil, err } + amount, err := to.GetCoins(params.SkycoinTicker) + if err != nil { + logWallet.WithError(err).Warnf("Couldn't get ticker %s from TransactionOutput", params.SkycoinTicker) + return nil, err + } strAmount := util.FormatCoins(amount, quotient) var txnOutput SkycoinTransactionOutput - txnOutput.skyOut.Address = to.String() + txnOutput.skyOut.Address = to.GetAddress().String() txnOutput.skyOut.Coins = strAmount addresses := make([]core.Address, 0) iterAddr, err := wlt.GetLoadedAddresses() @@ -1259,24 +1392,11 @@ func (wlt *LocalWallet) Transfer(destination core.TransactionOutput, options cor addresses = append(addresses, iterAddr.Value()) } - createTxnFunc := func(txnReq *api.CreateTransactionRequest) (core.Transaction, error) { - client, err := NewSkycoinApiClient(PoolSection) - if err != nil { - logWallet.WithError(err).Warn("Couldn't load api client") - return nil, err - } - defer ReturnSkycoinClient(client) - txnR, err := client.CreateTransaction(*txnReq) - if err != nil { - logWallet.WithError(err).Warn("Couldn't create transaction") - return nil, err - } - return fromTxnResponse(txnR), nil - } + createTxnFunc := skyAPICreateTxn return createTransaction(addresses, []core.TransactionOutput{&txnOutput}, nil, nil, options, createTxnFunc) - } -func (wlt LocalWallet) SendFromAddress(from []core.Address, to []core.TransactionOutput, change core.Address, options core.KeyValueStorage) (core.Transaction, error) { + +func (wlt LocalWallet) SendFromAddress(from []core.Address, to []core.TransactionOutput, change core.Address, options core.KeyValueStore) (core.Transaction, error) { logWallet.Info("Sending from addresses in local wallet") createTxnFunc := func(txnReq *api.CreateTransactionRequest) (core.Transaction, error) { client, err := NewSkycoinApiClient(PoolSection) @@ -1297,7 +1417,7 @@ func (wlt LocalWallet) SendFromAddress(from []core.Address, to []core.Transactio return createTransaction(from, to, nil, change, options, createTxnFunc) } -func (wlt LocalWallet) Spend(unspent, new []core.TransactionOutput, change core.Address, options core.KeyValueStorage) (core.Transaction, error) { +func (wlt LocalWallet) Spend(unspent, new []core.TransactionOutput, change core.Address, options core.KeyValueStore) (core.Transaction, error) { logWallet.Info("Spending from local wallet") createTxnFunc := func(txnReq *api.CreateTransactionRequest) (core.Transaction, error) { client, err := NewSkycoinApiClient(PoolSection) @@ -1391,7 +1511,12 @@ func (wlt *LocalWallet) GenAddresses(addrType core.AddressType, startIndex, coun if walletLoaded.IsEncrypted() { genAddressesInFile = func(w wallet.Wallet, n uint64) ([]cipher.Addresser, error) { - password, err := pwd("Insert Password") + pwdCtx := util.NewKeyValueMap() + pwdCtx.SetValue(core.StrTypeName, core.TypeNameWallet) + pwdCtx.SetValue(core.StrMethodName, "GenAddresses") + pwdCtx.SetValue(core.StrWalletName, wlt.Id) + pwdCtx.SetValue(core.StrWalletLabel, wlt.Label) + password, err := pwd("Enter password", pwdCtx) if err != nil { logWallet.WithError(err).Error("Something was wrong entering the password") return nil, nil @@ -1523,12 +1648,10 @@ func (wlt *LocalWallet) SignTransaction(txn core.Transaction, pwdReader core.Pas if strIdxs == nil { indices = nil } else { - indices = make([]int, len(strIdxs)) - for i, strIdx := range strIdxs { - indices[i], err = strconv.Atoi(strIdx) - if err != nil { - return nil, errors.ErrIntegerInputsRequired - } + indices, err = getHashIndices(txn.GetInputs(), strIdxs) + if err != nil { + logWallet.Error("Error parsing Skycoin transaction input indices array for signing") + return nil, err } } signedTxn, err = wlt.signSkycoinTxn(txn, pwdReader, indices) diff --git a/src/coin/skycoin/models/wallet_test.go b/src/coin/skycoin/models/wallet_test.go index bc08417b..49ece0b9 100644 --- a/src/coin/skycoin/models/wallet_test.go +++ b/src/coin/skycoin/models/wallet_test.go @@ -1,12 +1,15 @@ package skycoin import ( + "fmt" "io/ioutil" "math" "strconv" "strings" "testing" + "github.com/skycoin/skycoin/src/visor" + "github.com/fibercrypto/fibercryptowallet/src/coin/skycoin/params" "github.com/fibercrypto/fibercryptowallet/src/coin/skycoin/testsuite" "github.com/fibercrypto/fibercryptowallet/src/core" @@ -109,7 +112,7 @@ func TestSkycoinRemoteWalletCreateWallet(t *testing.T) { mockSkyApiCreateWallet(global_mock, &wltOpt2, "walletNonEncrypted", false) wltSrv := &SkycoinRemoteWallet{poolSection: PoolSection} - pwdReader := func(message string) (string, error) { + pwdReader := func(message string, _ core.KeyValueStore) (string, error) { return "pwd", nil } @@ -129,7 +132,7 @@ func TestSkycoinRemoteWalletEncrypt(t *testing.T) { global_mock.On("EncryptWallet", "wallet", "pwd").Return(&api.WalletResponse{}, nil) wltSrv := &SkycoinRemoteWallet{poolSection: PoolSection} - pwdReader := func(message string) (string, error) { + pwdReader := func(message string, _ core.KeyValueStore) (string, error) { return "pwd", nil } @@ -141,7 +144,7 @@ func TestSkycoinRemoteWalletDecrypt(t *testing.T) { global_mock.On("DecryptWallet", "wallet", "pwd").Return(&api.WalletResponse{}, nil) wltSrv := &SkycoinRemoteWallet{poolSection: PoolSection} - pwdReader := func(message string) (string, error) { + pwdReader := func(message string, _ core.KeyValueStore) (string, error) { return "pwd", nil } @@ -232,9 +235,10 @@ func TestRemoteWalletSignSkycoinTxn(t *testing.T) { wlt := &RemoteWallet{ Id: "wallet", + Encrypted: true, poolSection: PoolSection, } - pwdReader := func(message string) (string, error) { + pwdReader := func(string, core.KeyValueStore) (string, error) { return "password", nil } ret, err := wlt.Sign(&unTxn, nil, pwdReader, nil) @@ -556,7 +560,7 @@ func TestRemoteWalletGenAddresses(t *testing.T) { Id: "wallet", poolSection: PoolSection, } - pwdReader := func(message string) (string, error) { + pwdReader := func(message string, _ core.KeyValueStore) (string, error) { return "pwd", nil } iter := wlt.GenAddresses(0, 0, 2, pwdReader) @@ -801,7 +805,7 @@ func makeLocalWalletsFromKeyData(t *testing.T, keysData []KeyData) []core.Wallet var err error if w, isFound = walletsCache[kd.Mnemonic]; !isFound { if w = walletSet.GetWallet(walletID); w == nil { - w, err = walletSet.CreateWallet(walletID, kd.Mnemonic, wallet.WalletTypeDeterministic, false, func(string) (string, error) { return "", nil }, 0) + w, err = walletSet.CreateWallet(walletID, kd.Mnemonic, wallet.WalletTypeDeterministic, false, util.EmptyPassword, 0) require.NoError(t, err) } walletsCache[kd.Mnemonic] = w @@ -832,7 +836,7 @@ func TestTransactionSignInput(t *testing.T) { wallets := makeLocalWalletsFromKeyData(t, keysData) // Input is already signed - _, err = wallets[0].Sign(uiTxn, nil, util.EmptyPassword, []string{"0"}) + _, err = wallets[0].Sign(uiTxn, nil, util.EmptyPassword, []string{"#0"}) testutil.RequireError(t, err, "Transaction is fully signed") isFullySigned, err = uiTxn.IsFullySigned() require.NoError(t, err) @@ -843,7 +847,7 @@ func TestTransactionSignInput(t *testing.T) { isFullySigned, err = uiTxn.IsFullySigned() require.NoError(t, err) require.False(t, isFullySigned) - signedCoreTxn, err = wallets[1].Sign(uiTxn, nil, nil, []string{"1"}) + signedCoreTxn, err = wallets[1].Sign(uiTxn, nil, nil, []string{"#1"}) require.NoError(t, err) signedTxn, isUninjected := signedCoreTxn.(*SkycoinUninjectedTransaction) require.True(t, isUninjected) @@ -854,7 +858,25 @@ func TestTransactionSignInput(t *testing.T) { isFullySigned, err = signedTxn.IsFullySigned() require.NoError(t, err) require.True(t, isFullySigned) - _, err = wallets[1].Sign(signedTxn, nil, nil, []string{"1"}) + _, err = wallets[1].Sign(signedTxn, nil, nil, []string{"#1"}) + testutil.RequireError(t, err, "Transaction is fully signed") + // Repeat using UXID + isFullySigned, err = uiTxn.IsFullySigned() + require.NoError(t, err) + require.False(t, isFullySigned) + uxId := txn.In[1].Hex() + signedCoreTxn, err = wallets[1].Sign(uiTxn, nil, nil, []string{uxId}) + require.NoError(t, err) + signedTxn, isUninjected = signedCoreTxn.(*SkycoinUninjectedTransaction) + require.True(t, isUninjected) + require.NotEqual(t, uiTxn.txn, signedTxn.txn) + isFullySigned, err = uiTxn.IsFullySigned() + require.NoError(t, err) + require.False(t, isFullySigned) + isFullySigned, err = signedTxn.IsFullySigned() + require.NoError(t, err) + require.True(t, isFullySigned) + _, err = wallets[1].Sign(signedTxn, nil, nil, []string{"#1"}) testutil.RequireError(t, err, "Transaction is fully signed") // Transaction has no sigs; sigs array is initialized @@ -862,7 +884,7 @@ func TestTransactionSignInput(t *testing.T) { isFullySigned, err = uiTxn.IsFullySigned() require.NoError(t, err) require.False(t, isFullySigned) - signedCoreTxn, err = wallets[2].Sign(uiTxn, nil, nil, []string{"2"}) + signedCoreTxn, err = wallets[2].Sign(uiTxn, nil, nil, []string{"#2"}) require.NoError(t, err) signedTxn, isUninjected = signedCoreTxn.(*SkycoinUninjectedTransaction) require.True(t, isUninjected) @@ -876,14 +898,14 @@ func TestTransactionSignInput(t *testing.T) { require.False(t, signedTxn.txn.Sigs[2].Null()) // Signing the rest of the inputs individually works - signedCoreTxn, err = wallets[1].Sign(signedTxn, nil, nil, []string{"1"}) + signedCoreTxn, err = wallets[1].Sign(signedTxn, nil, nil, []string{"#1"}) require.NoError(t, err) signedTxn, isUninjected = signedCoreTxn.(*SkycoinUninjectedTransaction) require.True(t, isUninjected) isFullySigned, err = signedTxn.IsFullySigned() require.NoError(t, err) require.False(t, isFullySigned) - signedCoreTxn, err = wallets[0].Sign(signedTxn, nil, nil, []string{"0"}) + signedCoreTxn, err = wallets[0].Sign(signedTxn, nil, nil, []string{"#0"}) require.NoError(t, err) signedTxn, isUninjected = signedCoreTxn.(*SkycoinUninjectedTransaction) require.True(t, isUninjected) @@ -937,7 +959,7 @@ func TestTransactionSignInputs(t *testing.T) { // Valid signing h := txn.HashInner() - signedCoreTxn, err := wallet.Sign(uiTxn, nil, util.EmptyPassword, []string{"0", "1"}) + signedCoreTxn, err := wallet.Sign(uiTxn, nil, util.EmptyPassword, []string{"#0", "#1"}) require.NoError(t, err) signedTxn, isUninjected := signedCoreTxn.(*SkycoinUninjectedTransaction) require.True(t, isUninjected) @@ -971,6 +993,14 @@ func makeLocalWallet(t *testing.T) core.Wallet { return wallet } +func makeSkycoinBlockchain() core.BlockchainTransactionAPI { + return NewSkycoinBlockchain(0) +} + +func makeSkycoinSignService() core.BlockchainSignService { + return &SkycoinSignService{} +} + func TestLocalWalletTransfer(t *testing.T) { CleanGlobalMock() destinationAddress := testutil.MakeAddress() @@ -1201,3 +1231,318 @@ func TestSkycoinWalletTypes(t *testing.T) { require.Equal(t, wallet.WalletTypeBip44, wltSet.DefaultWalletType()) require.Equal(t, []string{wallet.WalletTypeDeterministic, wallet.WalletTypeBip44}, wltSet.SupportedWalletTypes()) } + +func TestSkycoinBlockchainSendFromAddress(t *testing.T) { + CleanGlobalMock() + + startAddress1 := testutil.MakeAddress() + startAddress2 := testutil.MakeAddress() + + destinationAddress := testutil.MakeAddress() + changeAddress := (testutil.MakeAddress()).String() + sky := 500 + hash := testutil.RandSHA256(t) + + toAddr := &SkycoinTransactionOutput{ + skyOut: readable.TransactionOutput{ + Address: destinationAddress.String(), + Coins: strconv.Itoa(sky), + Hours: uint64(250), + }, + } + fromAddr := []*SkycoinAddress{ + &SkycoinAddress{ + address: startAddress1.String(), + }, + &SkycoinAddress{ + address: startAddress2.String(), + }, + } + chgAddr := &SkycoinAddress{ + address: changeAddress, + } + + opt1 := NewTransferOptions() + opt1.SetValue("BurnFactor", "0.5") + opt1.SetValue("CoinHoursSelectionType", "auto") + + req1 := api.CreateTransactionRequest{ + IgnoreUnconfirmed: false, + HoursSelection: api.HoursSelection{ + Type: "auto", + Mode: "share", + ShareFactor: "0.5", + }, + ChangeAddress: &changeAddress, + To: []api.Receiver{ + api.Receiver{ + Address: destinationAddress.String(), + Coins: strconv.Itoa(sky), + }, + }, + Addresses: []string{startAddress1.String(), startAddress2.String()}, + } + + opt2 := NewTransferOptions() + opt2.SetValue("BurnFactor", "0.5") + opt2.SetValue("CoinHoursSelectionType", "manual") + + req2 := api.CreateTransactionRequest{ + IgnoreUnconfirmed: false, + HoursSelection: api.HoursSelection{ + Type: "manual", + }, + ChangeAddress: &changeAddress, + To: []api.Receiver{ + api.Receiver{ + Address: destinationAddress.String(), + Coins: strconv.Itoa(sky), + Hours: "250", + }, + }, + Addresses: []string{startAddress1.String(), startAddress2.String()}, + } + + txn := &coin.Transaction{ + Length: 100, + Type: 0, + InnerHash: hash, + } + ctxnR, err := api.NewCreateTransactionResponse(txn, nil) + ctxnR.Transaction.Fee = strconv.Itoa(sky) + require.NoError(t, err) + + mockSkyApiCreateTransaction(global_mock, &req1, ctxnR) + mockSkyApiCreateTransaction(global_mock, &req2, ctxnR) + + bc := makeSkycoinBlockchain() + wlt := &LocalWallet{} + + //Testing Hours selection to auto + from := []core.WalletAddress{makeSimpleWalletAddress(wlt, fromAddr[0]), makeSimpleWalletAddress(wlt, fromAddr[1])} + to := []core.TransactionOutput{toAddr} + txnResult, err := bc.SendFromAddress(from, to, chgAddr, opt1) + require.NoError(t, err) + require.NotNil(t, txnResult) + val, err := txnResult.ComputeFee(params.CoinHoursTicker) + require.NoError(t, err) + require.Equal(t, util.FormatCoins(uint64(sky), 10), util.FormatCoins(uint64(val), 10)) + require.Equal(t, ctxnR.Transaction.TxID, txnResult.GetId()) + + //Testing Hours selection to manual + from = []core.WalletAddress{makeSimpleWalletAddress(wlt, fromAddr[0]), makeSimpleWalletAddress(wlt, fromAddr[1])} + to = []core.TransactionOutput{toAddr} + txnResult, err = bc.SendFromAddress(from, to, chgAddr, opt2) + require.NoError(t, err) + require.NotNil(t, txnResult) + val, err = txnResult.ComputeFee(params.CoinHoursTicker) + require.NoError(t, err) + require.Equal(t, util.FormatCoins(uint64(sky), 10), util.FormatCoins(uint64(val), 10)) + require.Equal(t, ctxnR.Transaction.TxID, txnResult.GetId()) + +} + +func TestSkycoinBlockchainSpend(t *testing.T) { + CleanGlobalMock() + + hash := testutil.RandSHA256(t) + sky := 500 + //chgAddr := + changeAddr := testutil.MakeAddress().String() + chgAddr := &SkycoinAddress{ + address: changeAddr, + poolSection: "", + } + destinationAddress := testutil.MakeAddress() + + toAddr := &SkycoinTransactionOutput{ + skyOut: readable.TransactionOutput{ + Address: destinationAddress.String(), + Coins: strconv.Itoa(sky), + Hours: uint64(250), + }, + } + + uxOuts := make([]coin.UxOut, 2) + for i := 0; i < 2; i++ { + ux, _, err := makeUxOutWithSecret(t) + require.NoError(t, err) + uxOuts[i] = ux + } + + skyOuts := make([]core.TransactionOutput, len(uxOuts)) + for i := 0; i < len(uxOuts); i++ { + ux := uxOuts[i] + quot, err := util.AltcoinQuotient(params.SkycoinTicker) + require.NoError(t, err) + sky := util.FormatCoins(ux.Body.Coins, quot) + skOut := SkycoinTransactionOutput{ + spent: false, + skyOut: readable.TransactionOutput{ + Address: ux.Body.Address.String(), + Hash: ux.Body.Hash().String(), + Coins: sky, + Hours: ux.Body.Hours, + }, + } + skyOuts[i] = &skOut + } + + wltOuts := make([]core.WalletOutput, len(uxOuts)) + for i := 0; i < len(uxOuts); i++ { + wltOuts[i] = makeSimpleWalletOutput(nil, skyOuts[i]) + } + + uxOutsStr := make([]string, len(uxOuts)) + for i := 0; i < len(uxOuts); i++ { + uxOutsStr[i] = uxOuts[i].Hash().String() + } + + opt1 := NewTransferOptions() + opt1.SetValue("BurnFactor", "0.5") + opt1.SetValue("CoinHoursSelectionType", "auto") + + req1 := api.CreateTransactionRequest{ + UxOuts: uxOutsStr, + IgnoreUnconfirmed: false, + To: []api.Receiver{ + api.Receiver{ + Address: destinationAddress.String(), + Coins: strconv.Itoa(sky), + }, + }, + HoursSelection: api.HoursSelection{ + Type: "auto", + Mode: "share", + ShareFactor: "0.5", + }, + ChangeAddress: &changeAddr, + } + + opt2 := NewTransferOptions() + opt2.SetValue("BurnFactor", "0.5") + opt2.SetValue("CoinHoursSelectionType", "manual") + + req2 := api.CreateTransactionRequest{ + UxOuts: uxOutsStr, + IgnoreUnconfirmed: false, + HoursSelection: api.HoursSelection{ + Type: "manual", + }, + ChangeAddress: &changeAddr, + To: []api.Receiver{ + api.Receiver{ + Address: destinationAddress.String(), + Coins: strconv.Itoa(sky), + Hours: "250", + }, + }, + } + + txn := coin.Transaction{ + InnerHash: hash, + Type: 0, + Length: 100, + } + + crtTxn, err := api.NewCreateTransactionResponse(&txn, nil) + require.NoError(t, err) + crtTxn.Transaction.Fee = strconv.Itoa(sky) + + mockSkyApiCreateTransaction(global_mock, &req1, crtTxn) + mockSkyApiCreateTransaction(global_mock, &req2, crtTxn) + + bc := makeSkycoinBlockchain() + + to := []core.TransactionOutput{toAddr} + //Testing Hours selection auto + txnR, err := bc.Spend(wltOuts, to, chgAddr, opt1) + require.NoError(t, err) + require.NotNil(t, txnR) + require.Equal(t, txnR.GetId(), crtTxn.Transaction.TxID) + val, err := txnR.ComputeFee(params.CoinHoursTicker) + require.NoError(t, err) + require.Equal(t, util.FormatCoins(uint64(sky), 10), util.FormatCoins(uint64(val), 10)) + + //Testing Hours selection manual + txnR2, err := bc.Spend(wltOuts, to, chgAddr, opt2) + require.NoError(t, err) + require.NotNil(t, txnR2) + require.Equal(t, txnR2.GetId(), crtTxn.Transaction.TxID) + val2, err := txnR2.ComputeFee(params.CoinHoursTicker) + require.NoError(t, err) + require.Equal(t, util.FormatCoins(uint64(sky), 10), util.FormatCoins(uint64(val2), 10)) +} + +func TestSkycoinSignServiceSign(t *testing.T) { + CleanGlobalMock() + + txn, keyData, uxOuts := makeTransactionFromMultipleWallets(t, 3) + for _, ux := range uxOuts { + mockSkyApiUxOut(global_mock, ux) + } + + ins := make([]visor.TransactionInput, 0) + for _, out := range uxOuts { + in, err := visor.NewTransactionInput(out, out.Head.Time) + require.NoError(t, err) + + ins = append(ins, in) + } + + pwdReader := func(_ string, _ core.KeyValueStore) (string, error) { + return "", nil + } + + signer := makeSkycoinSignService() + wallets := makeLocalWalletsFromKeyData(t, keyData) + + require.NotEqual(t, wallets[0], wallets[1]) + + isds := make([]core.InputSignDescriptor, 0) + for i, wlt := range wallets { + descriptor := core.InputSignDescriptor{ + InputIndex: fmt.Sprintf("#%d", i), + SignerID: "", // Use wallet + Wallet: wlt, + } + isds = append(isds, descriptor) + + } + + //SkycoinCreatedTransaction + sigs := txn.Sigs + txn.Sigs = []cipher.Sig{} + apiCreTxn, err := api.NewCreatedTransaction(&txn, ins) + txn.Sigs = sigs + apiCreTxn.Sigs = make([]string, 0) + require.NoError(t, err) + require.NotNil(t, apiCreTxn) + require.Equal(t, apiCreTxn.InnerHash, txn.HashInner().Hex()) + skyCreTxn := NewSkycoinCreatedTransaction(*apiCreTxn) + + signedTxn, err := signer.Sign(skyCreTxn, isds, pwdReader) + require.NoError(t, err) + require.NotNil(t, signedTxn) + err = signedTxn.VerifySigned() + require.NoError(t, err) + //require.Equal(t, txn.Hash().String(), signedTxn.GetId()) + + //SkycoinUninjectedTransaction + txn.Sigs = []cipher.Sig{} + skyUninTxn := SkycoinUninjectedTransaction{ + txn: &txn, + fee: 300, + } + + signedTxn = nil + + signedTxn, err = signer.Sign(&skyUninTxn, isds, pwdReader) + require.NoError(t, err) + require.NotNil(t, signedTxn) + + signed, err := signedTxn.IsFullySigned() + require.NoError(t, err) + require.Equal(t, true, signed) + +} diff --git a/src/coin/skycoin/params/params.go b/src/coin/skycoin/params/params.go index 85615007..de28ab72 100644 --- a/src/coin/skycoin/params/params.go +++ b/src/coin/skycoin/params/params.go @@ -16,16 +16,24 @@ var ( // Constparams const ( - SkycoinTicker = "SKY" - SkycoinName = "Skycoin" - SkycoinFamily = "SkyFiber" - SkycoinDescription = "Skycoin is an entire cryptocurrency ecosystem aimed at eliminating mining rewards, developing energy-efficient custom hardware, speeding up transaction confirmation times, and the advancement of a more secure and private Internet" + // SkycoinTicker Skycoin coin identifier + SkycoinTicker = "SKY" + // SkycoinName human readable name associated to Skycoin + SkycoinName = "Skycoin" + // SkycoinFamily identifies Skyfiber coins + SkycoinFamily = "SkyFiber" + // SkycoinDescription verbose explanaitiion of Skycoin + SkycoinDescription = "Skycoin is an entire cryptocurrency ecosystem aimed at eliminating mining rewards, developing energy-efficient custom hardware, speeding up transaction confirmation times, and the advancement of a more secure and private Internet" + // CoinHoursTicker internal identifier to refer to Skycoin coin hours CoinHoursTicker = "SCH" - CoinHoursName = "Coin Hours" - CoinHoursFamily = "SkyFiber" - CoinHoursDescription = "Coin Hours is the parallel asset used for transaction fee, for creating scarcity, and to increase transaction privacy" + // CoinHoursName is the readable name for coin hours + CoinHoursName = "Coin Hours" + // CoinHoursDescription verbose explanaitiion of coin hours + CoinHoursDescription = "Coin Hours is the parallel asset used for transaction fee, for creating scarcity, and to increase transaction privacy" + // CoinHoursTicker internal identifier to refer to accumulated coin hours CalculatedHoursTicker = "SCH#ACC" - CalculatedHoursName = "Calculated Hours" - CalculatedHoursFamily = "SkyFiber" + // CoinHoursName is the readable name for accumulated coin hours + CalculatedHoursName = "Calculated Hours" + // CalculatedHoursDescription verbose explanaitiion of accumulated coin hours CalculatedHoursDescription = "Calculated Hours are Coin Hours calculated considering the time since an output was created" ) diff --git a/src/coin/skycoin/testsuite/env.go b/src/coin/skycoin/testsuite/env.go index 84030cc0..6e88c717 100644 --- a/src/coin/skycoin/testsuite/env.go +++ b/src/coin/skycoin/testsuite/env.go @@ -8,6 +8,7 @@ import ( const ( TestIDToken = "fibercryptotest" ManyAddressesFilename = "many-addresses.golden" + Seed0000Filename = "seed-0000.golden" ) func GetSkycoinCipherTestDataDir() string { diff --git a/src/core/meta.go b/src/core/blockchain.go similarity index 50% rename from src/core/meta.go rename to src/core/blockchain.go index 87acc092..952e9ba7 100644 --- a/src/core/meta.go +++ b/src/core/blockchain.go @@ -19,3 +19,13 @@ type BlockchainStatus interface { // GetNumberOfBlocks determine number of blocks in the blockchain GetNumberOfBlocks() (uint64, error) } + +// BlockchainAPI abstract interface for transactions management and utility functions for specific blockchain. +// The service should use the blockchain node to implement given interface. +type BlockchainTransactionAPI interface { + // SendFromAddress instantiates a transaction to send funds from specific source addresses + // to multiple destination addresses + SendFromAddress(from []WalletAddress, to []TransactionOutput, change Address, options KeyValueStore) (Transaction, error) + // Spend instantiate a transaction that spends specific outputs to send to multiple destination addresses + Spend(unspent []WalletOutput, new []TransactionOutput, change Address, options KeyValueStore) (Transaction, error) +} diff --git a/src/core/main.go b/src/core/main.go index c2b77ab5..7df3c731 100644 --- a/src/core/main.go +++ b/src/core/main.go @@ -42,6 +42,10 @@ type AltcoinPlugin interface { LoadWalletEnvs() []WalletEnv // LoadPEX instantiates proxy object to interact with nodes nodes of the P2P network LoadPEX(netType string) (PEX, error) + // LoadTransactionAPI blockchain transaction API entry poiny + LoadTransactionAPI(netType string) (BlockchainTransactionAPI, error) + // LoadSignService sign service entry point + LoadSignService() (BlockchainSignService, error) } // AltcoinManager defines the contract for altcoin repositories diff --git a/src/core/sign.go b/src/core/sign.go new file mode 100644 index 00000000..d510592c --- /dev/null +++ b/src/core/sign.go @@ -0,0 +1,17 @@ +package core + +// InputSignDescriptor specifies how to sign a specific transaction input +type InputSignDescriptor struct { + // InputIndex absolute (e.g. UXID) input identifier or relative (e.g. array index) in transaction context + InputIndex string + // SignerID selects a given signing strategy. If empty, default strategy will be chosen + SignerID UID + // Wallet placeholder containing private keys to sign transaction input + Wallet Wallet +} + +// BlockchainSignService implement multi-wallet transaction signing for the blockchain +type BlockchainSignService interface { + // Sign creates a new transaction by (fully or partially) signing a given transaction + Sign(txn Transaction, signSpec []InputSignDescriptor, pwd PasswordReader) (Transaction, error) +} diff --git a/src/core/storage.go b/src/core/storage.go index 4e17ee30..b1d7317b 100644 --- a/src/core/storage.go +++ b/src/core/storage.go @@ -1,9 +1,93 @@ package core -// KeyValueStorage provides read / write access to values given a key -type KeyValueStorage interface { +// KeyValueStore provides read / write access to values given a key +type KeyValueStore interface { // GetValue lookup value for key GetValue(key string) interface{} // SetValue bind value o known key SetValue(key string, value interface{}) } + +const ( + // StrSignerID option key for storing signer ID + StrSignerID = "signer.id" + // StrMethodType option key for method type name + StrTypeName = "api.typename" + // StrMethodName option key for method name + StrMethodName = "api.method" + // StrWalletLabel option key for wallet label + StrWalletLabel = "wallet.label" + // StrWalletName option key for wallet name + StrWalletName = "wallet.id" + // StrCoinTicker option key for coin ticker ID + StrCoinTicker = "coin.ticker" + // StrSenderObject option key for object that triggered an action + StrSenderObject = "call.self" + + // TypeNameAddress Address type name + TypeNameAddress = "Address" + // TypeNameAddressIterator AddressIterator type name + TypeNameAddressIterator = "AddressIterator" + // TypeNameAltcoinManager AltcoinManager type name + TypeNameAltcoinManager = "AltcoinManager" + // TypeNameAltcoinPlugin AltcoinPlugin type name + TypeNameAltcoinPlugin = "AltcoinPlugin" + // TypeNameBlock Block type name + TypeNameBlock = "Block" + // TypeNameBlockchainSignService BlockchainSignService type name + TypeNameBlockchainSignService = "BlockchainSignService" + // TypeNameBlockchainStatus BlockchainStatus type name + TypeNameBlockchainStatus = "BlockchainStatus" + // TypeNameBlockchainTransactionAPI BlockchainTransactionAPI type name + TypeNameBlockchainTransactionAPI = "BlockchainTransactionAPI" + // TypeNameCryptoAccount CryptoAccount type name + TypeNameCryptoAccount = "CryptoAccount" + // TypeNameKeyValueStore KeyValueStore type name + TypeNameKeyValueStore = "KeyValueStore" + // TypeNameMultiPool MultiPool type name + TypeNameMultiPool = "MultiPool" + // TypeNameMultiPoolSection MultiPoolSection type name + TypeNameMultiPoolSection = "MultiPoolSection" + // TypeNamePEX PEX type name + TypeNamePEX = "PEX" + // TypeNamePexNode PexNode type name + TypeNamePexNode = "PexNode" + // TypeNamePexNodeIterator PexNodeIterator type name + TypeNamePexNodeIterator = "PexNodeIterator" + // TypeNamePexNodeSet PexNodeSet type name + TypeNamePexNodeSet = "PexNodeSet" + // TypeNamePooledObjectFactory PooledObjectFactory type name + TypeNamePooledObjectFactory = "PooledObjectFactory" + // TypeNameSeedGenerator SeedGenerator type name + TypeNameSeedGenerator = "SeedGenerator" + // TypeNameTransaction Transaction type name + TypeNameTransaction = "Transaction" + // TypeNameTransactionInput TransactionInput type name + TypeNameTransactionInput = "TransactionInput" + // TypeNameTransactionInputIterator TransactionInputIterator type name + TypeNameTransactionInputIterator = "TransactionInputIterator" + // TypeNameTransactionIterator TransactionIterator type name + TypeNameTransactionIterator = "TransactionIterator" + // TypeNameTransactionOutput TransactionOutput type name + TypeNameTransactionOutput = "TransactionOutput" + // TypeNameTransactionOutputIterator TransactionOutputIterator type name + TypeNameTransactionOutputIterator = "TransactionOutputIterator" + // TypeNameTxnSigner TxnSigner type name + TypeNameTxnSigner = "TxnSigner" + // TypeNameTxnSignerIterator TxnSignerIterator type name + TypeNameTxnSignerIterator = "TxnSignerIterator" + // TypeNameWallet Wallet type name + TypeNameWallet = "Wallet" + // TypeNameWalletAddress WalletAddress type name + TypeNameWalletAddress = "WalletAddress" + // TypeNameWalletEnv WalletEnv type name + TypeNameWalletEnv = "WalletEnv" + // TypeNameWalletIterator WalletIterator type name + TypeNameWalletIterator = "WalletIterator" + // TypeNameWalletOutput WalletOutput type name + TypeNameWalletOutput = "WalletOutput" + // TypeNameWalletSet WalletSet type name + TypeNameWalletSet = "WalletSet" + // TypeNameWalletStorage WalletStorage type name + TypeNameWalletStorage = "WalletStorage" +) diff --git a/src/core/textutil.go b/src/core/textutil.go index c75f6dc2..4e1ddd2d 100644 --- a/src/core/textutil.go +++ b/src/core/textutil.go @@ -1,4 +1,4 @@ package core -// PasswordReadeer secure retrieval of passwords from users -type PasswordReader func(message string) (string, error) +// PasswordReader secure retrieval of passwords from users +type PasswordReader func(string, KeyValueStore) (string, error) diff --git a/src/core/wallet.go b/src/core/wallet.go index 536a9738..0b01f1a7 100644 --- a/src/core/wallet.go +++ b/src/core/wallet.go @@ -53,12 +53,12 @@ type Wallet interface { // SetLabel establishes a label for this wallet SetLabel(wltName string) // Transfer instantiates unsigned transaction to send funds from any wallet address to single destination - Transfer(to TransactionOutput, options KeyValueStorage) (Transaction, error) + Transfer(to TransactionOutput, options KeyValueStore) (Transaction, error) // SendFromAddress instantiates unsigned transaction to send funds from specific source addresses // to multiple destination addresses - SendFromAddress(from []Address, to []TransactionOutput, change Address, options KeyValueStorage) (Transaction, error) + SendFromAddress(from []Address, to []TransactionOutput, change Address, options KeyValueStore) (Transaction, error) // Spend instantiate unsigned transaction spending specific outputs to send to multiple destination addresses - Spend(unspent, new []TransactionOutput, change Address, options KeyValueStorage) (Transaction, error) + Spend(unspent, new []TransactionOutput, change Address, options KeyValueStore) (Transaction, error) // GenAddresses discover new addresses based on default hierarchically deterministic derivation sequences // FIXME: Support account index to be fully compatible with BIP44 GenAddresses(addrType AddressType, startIndex, count uint32, pwd PasswordReader) AddressIterator @@ -66,11 +66,27 @@ type Wallet interface { GetCryptoAccount() CryptoAccount // GetLoadedAddresses iterates over wallet addresses discovered and known to have previous history and coins GetLoadedAddresses() (AddressIterator, error) - // Sign creates a new transaction by (fully or partially) choosing a strategy to sign another transaction + // Sign creates a new transaction by (fully or partially) choosing a strategy to sign given transaction // If signer instance is nil then default wallet strategy should be used for signing Sign(txn Transaction, signer TxnSigner, pwd PasswordReader, index []string) (Transaction, error) } +// WalletOutput binds transaction output to originating wallet +type WalletOutput interface { + // GetWallet return wallet + GetWallet() Wallet + // GetOutput return transaction output. + GetOutput() TransactionOutput +} + +// WalletAddress aggregates address with originating wallet +type WalletAddress interface { + // GetWallet return wallet + GetWallet() Wallet + // GetOutput return transaction output. + GetAddress() Address +} + // SeedGenerator establishes the contract for generating BIP39-compatible mnemonics type SeedGenerator interface { // GenerateMnemonic generates a valid BIP-39 mnemonic phrase @@ -79,7 +95,7 @@ type SeedGenerator interface { VerifyMnemonic(seed string) (bool, error) } -// WalletEnvironment is the entry point to manage wallets +// WalletEnv is the entry point to manage wallets type WalletEnv interface { // GetStorage provides access to wallet data store GetStorage() WalletStorage diff --git a/src/errors/error.go b/src/errors/error.go index 1f1c0b2b..63e260f8 100644 --- a/src/errors/error.go +++ b/src/errors/error.go @@ -31,8 +31,12 @@ var ( ErrInvalidNetworkType = errors.New("Invalid netType") // ErrInvalidID invalid ID ErrInvalidID = errors.New("Invalid Id") + // ErrNotFound target item not found in collection + ErrNotFound = errors.New("Item not found in collection") // ErrParseTxID invalid string value for transaction hash ID ErrParseTxID = errors.New("Error parsing transaction hash") + // ErrParseSHA256 invalid SHA256 hash + ErrParseSHA256 = errors.New("Error parsing SHA256 hash") // ErrParseTxnFee invalid string value for transaction fee ErrParseTxnFee = errors.New("Error parsing transaction fee") // ErrParseTxnCoins transaction coins can not be parsed @@ -49,4 +53,6 @@ var ( ErrInvalidWalletEntropy = errors.New("Entropy must be 128 or 256") // ErrInvalidValue invalid value was supplied in to function ErrInvalidValue = errors.New("Value errors") + // ErrWalletCantSign wallet can not sign transactions + ErrWalletCantSign = errors.New("Wallet does not support transaction signing") ) diff --git a/src/models/blockchainModels.go b/src/models/blockchainModels.go index 4e1d939f..c6f8754b 100644 --- a/src/models/blockchainModels.go +++ b/src/models/blockchainModels.go @@ -6,6 +6,7 @@ import ( "github.com/fibercrypto/fibercryptowallet/src/coin/skycoin/models" //callable as skycoin "github.com/fibercrypto/fibercryptowallet/src/core" + "github.com/fibercrypto/fibercryptowallet/src/params" "github.com/fibercrypto/fibercryptowallet/src/util" qtcore "github.com/therecipe/qt/core" @@ -43,7 +44,7 @@ func (blockchainStatus *BlockchainStatusModel) init() { blockchainStatus.SetCurrentCoinHoursSupplyDefault("0") blockchainStatus.SetTotalCoinHoursSupplyDefault("0") blockchainStatus.SetLoading(true) - blockchainStatus.infoRequester = skycoin.NewSkycoinBlockchainStatus(1000000) //FIXME: set correct value + blockchainStatus.infoRequester = skycoin.NewSkycoinBlockchain(params.DataRefreshTimeout) } func (blockchainStatus *BlockchainStatusModel) update() { diff --git a/src/models/bridgeModel.go b/src/models/bridgeModel.go new file mode 100644 index 00000000..3a2823df --- /dev/null +++ b/src/models/bridgeModel.go @@ -0,0 +1,53 @@ +package models + +import ( + "sync" + + "github.com/therecipe/qt/core" +) + +type QBridge struct { + core.QObject + _ func() `constructor:"init"` + _ func() `slot:"lock"` + _ func() `slot:"unlock"` + _ func(message string) `signal:"getPassword"` + _ func(string) `slot:"setResult"` + _ func() string `slot:"getResult"` + result string + sem sync.Mutex + use sync.Mutex +} + +func (b *QBridge) init() { + + b.ConnectLock(b.lock) + b.ConnectUnlock(b.unlock) + b.ConnectSetResult(b.setResult) + b.ConnectGetResult(b.getResult) +} + +func (b *QBridge) BeginUse() { + b.use.Lock() +} + +func (b *QBridge) EndUse() { + b.use.Unlock() +} + +func (b *QBridge) lock() { + b.sem.Lock() + +} + +func (b *QBridge) setResult(result string) { + b.result = result +} + +func (b *QBridge) getResult() string { + return b.result +} + +func (b *QBridge) unlock() { + b.sem.Unlock() +} diff --git a/src/models/models.go b/src/models/models.go index 92b81b8a..3056ab3f 100644 --- a/src/models/models.go +++ b/src/models/models.go @@ -15,4 +15,6 @@ func init() { ModelAddresses_QmlRegisterType2("OutputsModels", 1, 0, "QAddresses") ModelOutputs_QmlRegisterType2("OutputsModels", 1, 0, "QOutputs") QTransaction_QmlRegisterType2("Transactions", 1, 0, "QTransaction") + QBridge_QmlRegisterType2("Utils", 1, 0, "QBridge") + } diff --git a/src/models/walletsManager.go b/src/models/walletsManager.go index c13dec1d..030952bd 100644 --- a/src/models/walletsManager.go +++ b/src/models/walletsManager.go @@ -30,31 +30,35 @@ type WalletManager struct { addresseseByWallets map[string][]*QAddress outputsByAddress map[string][]*QOutput altManager core.AltcoinManager - _ func() `slot:"updateWalletEnvs"` - _ func(wltId, address string) `slot:"updateOutputs"` - _ func(string) `slot:"updateAddresses"` - _ func() `slot:"updateWallets"` - _ func() `constructor:"init"` - _ func(seed string, label string, walletType string, password string, scanN int) *QWallet `slot:"createEncryptedWallet"` - _ func(seed string, label string, walletType string, scanN int) *QWallet `slot:"createUnencryptedWallet"` - _ func(entropy int) string `slot:"getNewSeed"` - _ func(seed string) int `slot:"verifySeed"` - _ func(id string, n int, password string) `slot:"newWalletAddress"` - _ func(id string, password string) int `slot:"encryptWallet"` - _ func(id string, password string) int `slot:"decryptWallet"` - _ func() []*QWallet `slot:"getWallets"` - _ func(id string) []*QAddress `slot:"getAddresses"` - _ func(id string, source string, password string, index []int, qTxn *QTransaction) *QTransaction `slot:"signTxn"` - _ func(wltId string, destinationAddress string, amount string) *QTransaction `slot:"sendTo"` - _ func(id, label string) *QWallet `slot:"editWallet"` - _ func(wltId, address string) []*QOutput `slot:"getOutputs"` - _ func(txn *QTransaction) bool `slot:"broadcastTxn"` - _ func(wltId string, from, addrTo, skyTo, coinHoursTo []string, change string, automaticCoinHours bool, burnFactor string) *QTransaction `slot:"sendFromAddresses"` - _ func(wltId string, outs, addrTo, skyTo, coinHoursTo []string, change string, automaticCoinHours bool, burnFactor string) *QTransaction `slot:"sendFromOutputs"` - _ func() []*QAddress `slot:"getAllAddresses"` - _ func(wltId string) []*QOutput `slot:"getOutputsFromWallet"` - _ func() string `slot:"getDefaultWalletType"` - _ func() []string `slot:"getAvailableWalletTypes"` + signer core.BlockchainSignService + transactionAPI core.BlockchainTransactionAPI + + _ func() `slot:"updateWalletEnvs"` + _ func(wltId, address string) `slot:"updateOutputs"` + _ func(string) `slot:"updateAddresses"` + _ func() `slot:"updateWallets"` + _ func() `constructor:"init"` + _ func(seed string, label string, walletType string, password string, scanN int) *QWallet `slot:"createEncryptedWallet"` + _ func(seed string, label string, walletType string, scanN int) *QWallet `slot:"createUnencryptedWallet"` + _ func(entropy int) string `slot:"getNewSeed"` + _ func(seed string) int `slot:"verifySeed"` + _ func(id string, n int, password string) `slot:"newWalletAddress"` + _ func(id string, password string) int `slot:"encryptWallet"` + _ func(id string, password string) int `slot:"decryptWallet"` + _ func() []*QWallet `slot:"getWallets"` + _ func(id string) []*QAddress `slot:"getAddresses"` + _ func(wltIds, addresses []string, source string, pwd interface{}, index []int, qTxn *QTransaction) *QTransaction `slot:"signTxn"` + _ func(wltId string, destinationAddress string, amount string) *QTransaction `slot:"sendTo"` + _ func(id, label string) *QWallet `slot:"editWallet"` + _ func(wltId, address string) []*QOutput `slot:"getOutputs"` + _ func(txn *QTransaction) bool `slot:"broadcastTxn"` + _ func(wltIds, from, addrTo, skyTo, coinHoursTo []string, change string, automaticCoinHours bool, burnFactor string) *QTransaction `slot:"sendFromAddresses"` + _ func(wltIds, outs, addrTo, skyTo, coinHoursTo []string, change string, automaticCoinHours bool, burnFactor string) *QTransaction `slot:"sendFromOutputs"` + _ func() []*QAddress `slot:"getAllAddresses"` + _ func(wltId string) []*QOutput `slot:"getOutputsFromWallet"` + _ func() string `slot:"getDefaultWalletType"` + _ func(wltIds, addresses []string, source string, bridgeForPassword *QBridge, index []int, qTxn *QTransaction) `slot:"signAndBroadcastTxnAsync"` + _ func() []string `slot:"getAvailableWalletTypes"` } func (walletM *WalletManager) init() { @@ -83,15 +87,17 @@ func (walletM *WalletManager) init() { walletM.ConnectUpdateWallets(walletM.updateWallets) walletM.ConnectUpdateAddresses(walletM.updateAddresses) walletM.ConnectUpdateOutputs(walletM.updateOutputs) + walletM.ConnectSignAndBroadcastTxnAsync(walletM.signAndBroadcastTxnAsync) walletM.ConnectGetDefaultWalletType(walletM.getDefaultWalletType) walletM.ConnectGetAvailableWalletTypes(walletM.getAvailableWalletTypes) walletM.addresseseByWallets = make(map[string][]*QAddress, 0) walletM.outputsByAddress = make(map[string][]*QOutput, 0) walletM.altManager = local.LoadAltcoinManager() - walletM.SeedGenerator = new(sky.SeedService) walletManager = walletM walletM.updateWalletEnvs() + walletM.updateSigner() + walletM.updateTransactionAPI() }) walletM = walletManager @@ -120,6 +126,35 @@ func (walletM *WalletManager) getAvailableWalletTypes() []string { return walletM.WalletEnv.GetWalletSet().SupportedWalletTypes() } +func (walletM *WalletManager) updateSigner() { + logWalletManager.Info("Updating Signers") + signers := make([]core.BlockchainSignService, 0) + + for _, plug := range walletM.altManager.ListRegisteredPlugins() { + sing, err := plug.LoadSignService() + if err != nil { + logWalletManager.WithError(err).Errorf("Error loading signer from %s plugin", plug.GetName()) + } + signers = append(signers, sing) + } + + walletM.signer = signers[0] +} + +func (walletM *WalletManager) updateTransactionAPI() { + logWalletManager.Info("Updating TransactionAPI") + txnAPIS := make([]core.BlockchainTransactionAPI, 0) + + for _, plug := range walletM.altManager.ListRegisteredPlugins() { + txnAPI, err := plug.LoadTransactionAPI("MainNet") + if err != nil { + logWalletManager.WithError(err).Errorf("Error loading transaction API from %s plugin", plug.GetName()) + } + txnAPIS = append(txnAPIS, txnAPI) + } + + walletM.transactionAPI = txnAPIS[0] +} func (walletM *WalletManager) updateWalletEnvs() { logWalletManager.Info("Updating WalletEnvs") walletsEnvs := make([]core.WalletEnv, 0) @@ -294,6 +329,15 @@ func (walletM *WalletManager) broadcastTxn(txn *QTransaction) bool { logWalletManager.WithError(err).Warn("Error loading PEX") return false } + isSigned, err := txn.txn.IsFullySigned() + if err != nil { + logWalletManager.WithError(err).Warn("Error checking if transaction if fully signed") + return false + } + if !isSigned { + logWalletManager.Warn("Transaction is not fully signed") + return false + } err = pex.BroadcastTxn(txn.txn) if err != nil { logWalletManager.WithError(err).Warn("Error broadcasting transaction") @@ -303,17 +347,27 @@ func (walletM *WalletManager) broadcastTxn(txn *QTransaction) bool { return true } -func (walletM *WalletManager) sendFromOutputs(wltId string, from, addrTo, skyTo, coinHoursTo []string, change string, automaticCoinHours bool, burnFactor string) *QTransaction { +func (walletM *WalletManager) sendFromOutputs(wltIds []string, from, addrTo, skyTo, coinHoursTo []string, change string, automaticCoinHours bool, burnFactor string) *QTransaction { logWalletManager.Info("Creating transaction") - wlt := walletM.WalletEnv.GetWalletSet().GetWallet(wltId) - if wlt == nil { - logWalletManager.Warn("Couldn't load wallet to create transaction") - return nil + wltCache := make(map[string]core.Wallet, 0) + wlts := make([]core.Wallet, 0) + for _, wltId := range wltIds { + var wlt core.Wallet + wlt, exist := wltCache[wltId] + if !exist { + wlt = walletM.WalletEnv.GetWalletSet().GetWallet(wltId) + if wlt == nil { + logWalletManager.Warn("Couldn't load wallet to create transaction") + return nil + } + wltCache[wltId] = wlt + } + wlts = append(wlts, wlt) } + outputsFrom := make([]core.TransactionOutput, 0) for _, outAddr := range from { - addr := util.NewGenericAddress(outAddr) - out := util.NewGenericOutput(&addr) + out := util.NewGenericOutput(nil, outAddr) outputsFrom = append(outputsFrom, &out) } outputsTo := make([]core.TransactionOutput, 0) @@ -323,7 +377,7 @@ func (walletM *WalletManager) sendFromOutputs(wltId string, from, addrTo, skyTo, ch = coinHoursTo[i] } addr := util.NewGenericAddress(addrTo[i]) - out := util.NewGenericOutput(&addr) + out := util.NewGenericOutput(&addr, "") // FIXME: Remove explicit reference to Skycoin err := out.PushCoins(sky.Sky, skyTo[i]) if err != nil { @@ -346,8 +400,23 @@ func (walletM *WalletManager) sendFromOutputs(wltId string, from, addrTo, skyTo, } else { opt.SetValue("CoinHoursSelectionType", "manual") } + var txn core.Transaction + var err error + if len(wltCache) > 1 { + fmt.Println("MULTI TXN") + walletsOutputs := make([]core.WalletOutput, 0) + for i, wlt := range wlts { + walletsOutputs = append(walletsOutputs, &util.SimpleWalletOutput{ + Wallet: wlt, + UxOut: outputsFrom[i], + }) + } + txn, err = walletM.transactionAPI.Spend(walletsOutputs, outputsTo, &changeAddr, opt) + } else { + fmt.Println("SIMPLE TXN") + txn, err = wlts[0].Spend(outputsFrom, outputsTo, &changeAddr, opt) + } - txn, err := wlt.Spend(outputsFrom, outputsTo, &changeAddr, opt) if err != nil { logWalletManager.WithError(err).Info("Error creating transaction") return nil @@ -360,12 +429,24 @@ func (walletM *WalletManager) sendFromOutputs(wltId string, from, addrTo, skyTo, } return qTransaction } -func (walletM *WalletManager) sendFromAddresses(wltId string, from, addrTo, skyTo, coinHoursTo []string, change string, automaticCoinHours bool, burnFactor string) *QTransaction { - wlt := walletM.WalletEnv.GetWalletSet().GetWallet(wltId) - if wlt == nil { - logWalletManager.Warn("Couldn't load wallet to create transaction") - return nil +func (walletM *WalletManager) sendFromAddresses(wltIds []string, from, addrTo, skyTo, coinHoursTo []string, change string, automaticCoinHours bool, burnFactor string) *QTransaction { + fmt.Printf("WLTS %v\n", wltIds) + wltCache := make(map[string]core.Wallet, 0) + wlts := make([]core.Wallet, 0) + for _, wltId := range wltIds { + var wlt core.Wallet + wlt, exist := wltCache[wltId] + if !exist { + wlt = walletM.WalletEnv.GetWalletSet().GetWallet(wltId) + if wlt == nil { + logWalletManager.Warn("Couldn't load wallet to create transaction") + return nil + } + wltCache[wltId] = wlt + } + wlts = append(wlts, wlt) } + addrsFrom := make([]core.Address, 0) for _, addr := range from { @@ -378,17 +459,17 @@ func (walletM *WalletManager) sendFromAddresses(wltId string, from, addrTo, skyT ch = coinHoursTo[i] } addr := util.NewGenericAddress(addrTo[i]) - out := util.NewGenericOutput(&addr) + out := util.NewGenericOutput(&addr, "") // FIXME: Remove explicit reference to Skycoin err := out.PushCoins(sky.Sky, skyTo[i]) if err != nil { - logWalletManager.WithError(err).Warn("Error parsing value for %s", sky.Sky) + logWalletManager.WithError(err).Warnf("Error parsing value for %s", sky.Sky) return nil } // FIXME: Remove explicit reference to Skycoin err = out.PushCoins(sky.CoinHour, ch) if err != nil { - logWalletManager.WithError(err).Warn("Error parsing value for %s", sky.Sky) + logWalletManager.WithError(err).Warnf("Error parsing value for %s", sky.Sky) return nil } outputsTo = append(outputsTo, &out) @@ -402,8 +483,23 @@ func (walletM *WalletManager) sendFromAddresses(wltId string, from, addrTo, skyT } else { opt.SetValue("CoinHoursSelectionType", "manual") } + var txn core.Transaction + var err error + if len(wltCache) > 1 { + walletsAddresses := make([]core.WalletAddress, 0) + for i, wlt := range wlts { + walletsAddresses = append(walletsAddresses, &util.SimpleWalletAddress{ + Wallet: wlt, + UxOut: addrsFrom[i], + }) + } + fmt.Println("MULTIPLE TRANSACTION") + txn, err = walletM.transactionAPI.SendFromAddress(walletsAddresses, outputsTo, changeAddr, opt) + } else { + fmt.Println("SINGLE TRANSACTION") + txn, err = wlts[0].SendFromAddress(addrsFrom, outputsTo, changeAddr, opt) + } - txn, err := wlt.SendFromAddress(addrsFrom, outputsTo, changeAddr, opt) if err != nil { logWalletManager.WithError(err).Info("Error creating transaction") return nil @@ -453,7 +549,7 @@ func (walletM *WalletManager) sendTo(wltId, destinationAddress, amount string) * logWalletManager.Warn("Couldn't load wallet to create transaction") return nil } - txOut := util.NewGenericOutput(&addr) + txOut := util.NewGenericOutput(&addr, "") // FIXME: Remove explicit reference to Skycoin err := txOut.PushCoins(sky.Sky, amount) if err != nil { @@ -475,35 +571,115 @@ func (walletM *WalletManager) sendTo(wltId, destinationAddress, amount string) * } -func (walletM *WalletManager) signTxn(id, source, password string, index []int, qTxn *QTransaction) *QTransaction { +func (walletM *WalletManager) signTxn(wltIds, address []string, source string, tmpPwd interface{}, index []int, qTxn *QTransaction) *QTransaction { + pwd, isPwdReader := tmpPwd.(core.PasswordReader) + if !isPwdReader { + return nil + } logWalletManager.Info("Signig transaction") - // Get wallet - wlt := walletM.WalletEnv.GetWalletSet().GetWallet(id) - if wlt == nil { - logWalletManager.Warn("Couldn't load wallet to Sign transaction") + + if len(wltIds) != len(address) { + logWalletManager.Error("Wallets and addresses provided are incorrect") return nil } - signer := util.LookupSignerByUID(wlt, core.UID(source)) - txn, err := wlt.Sign(qTxn.txn, signer, func(message string) (string, error) { - return password, nil - }, nil) // TODO Get index for sign specific txn indexes + + wltCache := make(map[string]core.Wallet) + wltByAddr := make(map[string]core.Wallet) + wlts := make([]core.Wallet, 0) + + for i, wltId := range wltIds { + var wlt core.Wallet + wlt, exist := wltCache[wltId] + if !exist { + wlt = walletM.WalletEnv.GetWalletSet().GetWallet(wltId) + if wlt == nil { + logWalletManager.Warn("Couldn't load wallet to Sign transaction") + return nil + } + wltCache[wltId] = wlt + } + wltByAddr[address[i]] = wlt + wlts = append(wlts, wlt) + } + + var txn core.Transaction + var err error + + if len(wltCache) > 1 { + signDescriptors := make([]core.InputSignDescriptor, 0) + for _, in := range qTxn.txn.GetInputs() { + sd := core.InputSignDescriptor{ + InputIndex: in.GetId(), + SignerID: core.UID(source), + Wallet: wltByAddr[in.GetSpentOutput().GetAddress().String()], + } + signDescriptors = append(signDescriptors, sd) + } + txn, err = walletM.signer.Sign(qTxn.txn, signDescriptors, pwd) + } else { + signer, err2 := util.LookupSignServiceForWallet(wlts[0], core.UID(source)) + if err2 != nil { + logWalletManager.WithError(err).Warnf("No signer %s for wallet %v", source, wlts[0]) + return nil + } + txn, err = wlts[0].Sign(qTxn.txn, signer, pwd, nil) + } + if err != nil { logWalletManager.WithError(err).Warn("Error signing txn") return nil } + qTxn, err = NewQTransactionFromTransaction(txn) if err != nil { logWalletManager.WithError(err).Warn("Error converting transaction") return nil } return qTxn + +} + +func (walletM *WalletManager) signAndBroadcastTxnAsync(wltIds, addresses []string, source string, bridgeForPassword *QBridge, index []int, qTxn *QTransaction) { + channel := make(chan *QTransaction) + go func() { + pwd := func(message string, ctx core.KeyValueStore) (string, error) { + bridgeForPassword.BeginUse() + defer bridgeForPassword.EndUse() + bridgeForPassword.lock() + suffix := "" + v := ctx.GetValue(core.StrWalletLabel) + if v == nil { + v = ctx.GetValue(core.StrWalletName) + } + if v != nil { + if str, isStr := v.(string); isStr { + suffix = " for " + str + } + } + bridgeForPassword.GetPassword(message + suffix) + bridgeForPassword.lock() + pass := bridgeForPassword.getResult() + bridgeForPassword.unlock() + return pass, nil + } + + channel <- walletM.signTxn(wltIds, addresses, source, pwd, index, qTxn) + }() + + go func() { + txn := <-channel + if txn != nil { + walletM.broadcastTxn(txn) + } + + }() } func (walletM *WalletManager) createEncryptedWallet(seed, label, wltType, password string, scanN int) *QWallet { logWalletManager.Info("Creating encrypted wallet") - pwd := func(message string) (string, error) { - return password, nil - } + pwd := util.ConstantPassword(password) + // NOTE: No easy way to get plain passwords in memory + password = "" fmt.Println("WALLET TYPE ", wltType) wlt, err := walletM.WalletEnv.GetWalletSet().CreateWallet(label, seed, wltType, true, pwd, scanN) if err != nil { @@ -518,9 +694,7 @@ func (walletM *WalletManager) createEncryptedWallet(seed, label, wltType, passwo func (walletM *WalletManager) createUnencryptedWallet(seed, label, wltType string, scanN int) *QWallet { logWalletManager.Info("Creating encrypted wallet") - pwd := func(message string) (string, error) { - return "", nil - } + pwd := util.EmptyPassword wlt, err := walletM.WalletEnv.GetWalletSet().CreateWallet(label, seed, wltType, false, pwd, scanN) if err != nil { @@ -560,9 +734,9 @@ func (walletM *WalletManager) verifySeed(seed string) int { func (walletM *WalletManager) encryptWallet(id, password string) int { logWalletManager.Info("Encrypting wallet") - pwd := func(message string) (string, error) { - return password, nil - } + pwd := util.ConstantPassword(password) + // NOTE: No easy way to get plain passwords in memory + password = "" walletM.WalletEnv.GetStorage().Encrypt(id, pwd) ret, err := walletM.WalletEnv.GetStorage().IsEncrypted(id) if err != nil { @@ -577,9 +751,9 @@ func (walletM *WalletManager) encryptWallet(id, password string) int { func (walletM *WalletManager) decryptWallet(id, password string) int { logWalletManager.Info("Decrypt wallet") - pwd := func(message string) (string, error) { - return password, nil - } + pwd := util.ConstantPassword(password) + // NOTE: No easy way to get plain passwords in memory + password = "" walletM.WalletEnv.GetStorage().Decrypt(id, pwd) ret, err := walletM.WalletEnv.GetStorage().IsEncrypted(id) if err != nil { @@ -595,9 +769,9 @@ func (walletM *WalletManager) decryptWallet(id, password string) int { func (walletM *WalletManager) newWalletAddress(id string, n int, password string) { logWalletManager.Info("Creating new wallet addresses") wlt := walletM.WalletEnv.GetWalletSet().GetWallet(id) - pwd := func(message string) (string, error) { - return password, nil - } + pwd := util.ConstantPassword(password) + // NOTE: No easy way to get plain passwords in memory + password = "" wltEntriesLen := 0 it, err := wlt.GetLoadedAddresses() if err != nil { diff --git a/src/params/params.go b/src/params/params.go new file mode 100644 index 00000000..02d80422 --- /dev/null +++ b/src/params/params.go @@ -0,0 +1,6 @@ +package params + +const ( + // Default refresh timeout for API data + DataRefreshTimeout = 1000000 //FIXME: set correct value +) diff --git a/src/ui/Controls/TextArea.qml b/src/ui/Controls/TextArea.qml index 2801b083..3caabe4c 100644 --- a/src/ui/Controls/TextArea.qml +++ b/src/ui/Controls/TextArea.qml @@ -29,4 +29,4 @@ TextArea { contextMenu.popup() } } -} \ No newline at end of file +} diff --git a/src/ui/Controls/TextField.qml b/src/ui/Controls/TextField.qml index d082156a..4294ebfa 100644 --- a/src/ui/Controls/TextField.qml +++ b/src/ui/Controls/TextField.qml @@ -29,4 +29,4 @@ TextField { contextMenu.popup() } } -} \ No newline at end of file +} diff --git a/src/ui/CustomMenuItem.qml b/src/ui/CustomMenuItem.qml index 0d3e48e2..9b4b207b 100644 --- a/src/ui/CustomMenuItem.qml +++ b/src/ui/CustomMenuItem.qml @@ -26,4 +26,4 @@ MenuItem { opacity: parent.highlighted ? 1.0 : 0.0 Behavior on opacity { NumberAnimation { duration: 500; easing.type: Easing.OutQuint } } } -} \ No newline at end of file +} diff --git a/src/ui/Delegates/OutputsListAddressDelegate.qml b/src/ui/Delegates/OutputsListAddressDelegate.qml index 6c1ed619..900d77b5 100644 --- a/src/ui/Delegates/OutputsListAddressDelegate.qml +++ b/src/ui/Delegates/OutputsListAddressDelegate.qml @@ -83,4 +83,4 @@ Item { } // ListView // Roles: outputID, addressSky, addressCoinHours -} \ No newline at end of file +} diff --git a/src/ui/Delegates/PasswordRequesterDelegate.qml b/src/ui/Delegates/PasswordRequesterDelegate.qml new file mode 100644 index 00000000..c2f2528a --- /dev/null +++ b/src/ui/Delegates/PasswordRequesterDelegate.qml @@ -0,0 +1,34 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Controls.Material 2.12 +import QtQuick.Layouts 1.12 +import OutputsModels 1.0 + + +Item { + id: passwordRequesterDelegate + + TextField { + id: textFieldPassword + text: name + placeholderText: qsTr("Password") + selectByMouse: true + echoMode: TextField.Password + focus: true + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + } + + Button { + id: buttonForgot + text: qsTr("I forgot my password") + flat: true + highlighted: hovered + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.fillWidth: true + + onClicked: { + passwordForgotten() + } + } +} diff --git a/src/ui/Dialogs/DialogAddAddresses.qml b/src/ui/Dialogs/DialogAddAddresses.qml index 611bd0e2..749619cd 100644 --- a/src/ui/Dialogs/DialogAddAddresses.qml +++ b/src/ui/Dialogs/DialogAddAddresses.qml @@ -33,4 +33,4 @@ Dialog { focus: true } } // ColumnLayout (root) -} \ No newline at end of file +} diff --git a/src/ui/Dialogs/DialogAddLoadWallet.qml b/src/ui/Dialogs/DialogAddLoadWallet.qml index 12e9181d..1d50cc66 100644 --- a/src/ui/Dialogs/DialogAddLoadWallet.qml +++ b/src/ui/Dialogs/DialogAddLoadWallet.qml @@ -230,4 +230,4 @@ Dialog { anchors.rightMargin: -dialogAddWallet.rightPadding + 1 } } // Flickable -} \ No newline at end of file +} diff --git a/src/ui/Dialogs/DialogEditWallet.qml b/src/ui/Dialogs/DialogEditWallet.qml index bd11c281..1a3de027 100644 --- a/src/ui/Dialogs/DialogEditWallet.qml +++ b/src/ui/Dialogs/DialogEditWallet.qml @@ -40,4 +40,4 @@ Dialog { standardButton(Dialog.Ok).enabled = text && text != originalWalletName } } -} \ No newline at end of file +} diff --git a/src/ui/Dialogs/DialogSelectAddressByWallet.qml b/src/ui/Dialogs/DialogSelectAddressByWallet.qml index 4d1ba3f4..8525fc44 100644 --- a/src/ui/Dialogs/DialogSelectAddressByWallet.qml +++ b/src/ui/Dialogs/DialogSelectAddressByWallet.qml @@ -90,4 +90,4 @@ Dialog { ScrollIndicator.vertical: ScrollIndicator { } } // ListView } // ColumnLayout -} \ No newline at end of file +} diff --git a/src/ui/MenuThemeAccent.qml b/src/ui/MenuThemeAccent.qml index 65f52c11..13594e93 100644 --- a/src/ui/MenuThemeAccent.qml +++ b/src/ui/MenuThemeAccent.qml @@ -125,4 +125,4 @@ Menu { } // Rectangle } // Repeater } // Grid -} \ No newline at end of file +} diff --git a/src/ui/MultiplePasswordRequester.qml b/src/ui/MultiplePasswordRequester.qml new file mode 100644 index 00000000..bbace1d2 --- /dev/null +++ b/src/ui/MultiplePasswordRequester.qml @@ -0,0 +1,56 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Controls.Material 2.12 +import QtQuick.Layouts 1.12 + +// Resource imports +// import "qrc:/ui/src/ui/Controls" +import "Controls" // For quick UI development, switch back to resources when making a release + +Item { + id: root + + property alias text: textFieldPassword.text + property alias placeholderText: textFieldPassword.placeholderText + property alias allowPasswordRecovery: buttonForgot.visible + + signal passwordForgotten() + + function forceTextFocus() { + textFieldPassword.forceActiveFocus() + } + + function clear() { + textFieldPassword.clear() + } + + implicitHeight: textFieldPassword.implicitHeight + buttonForgot.implicitHeight + + ColumnLayout { + anchors.fill: parent + + TextField { + id: textFieldPassword + + placeholderText: qsTr("Password") + selectByMouse: true + echoMode: TextField.Password + focus: true + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + } + + Button { + id: buttonForgot + text: qsTr("I forgot my password") + flat: true + highlighted: hovered + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.fillWidth: true + + onClicked: { + passwordForgotten() + } + } + } +} diff --git a/src/ui/PageSend.qml b/src/ui/PageSend.qml index da60bc82..57c0f47a 100644 --- a/src/ui/PageSend.qml +++ b/src/ui/PageSend.qml @@ -3,6 +3,7 @@ import QtQuick.Controls 2.12 import QtQuick.Controls.Material 2.12 import QtQuick.Layouts 1.12 import Transactions 1.0 +import Utils 1.0 // Resource imports // import "qrc:/ui/src/ui/Dialogs" @@ -35,33 +36,38 @@ Page { var isEncrypted var walletSelected + var walletSelecteds if (advancedMode){ - var outs = stackView.currentItem.advancedPage.getSelectedOutputs() - var addrs = stackView.currentItem.advancedPage.getSelectedAddresses() - walletSelected = stackView.currentItem.advancedPage.getSelectedWallet()[0] + var outs = stackView.currentItem.advancedPage.getSelectedOutputsWithWallets() + var addrs = stackView.currentItem.advancedPage.getSelectedAddressesWithWallets() + //walletSelecteds = stackView.currentItem.advancedPage.getSelectedWallet() var destinationSummary = stackView.currentItem.advancedPage.getDestinationsSummary() var changeAddress = stackView.currentItem.advancedPage.getChangeAddress() var automaticCoinHours = stackView.currentItem.advancedPage.getAutomaticCoinHours() var burnFactor = stackView.currentItem.advancedPage.getBurnFactor() - if (outs.length > 0){ + if (outs[0].length > 0){ console.log(outs) - txn = walletManager.sendFromOutputs(walletSelected, outs, destinationSummary[0], destinationSummary[1], destinationSummary[2], changeAddress, automaticCoinHours, burnFactor) + txn = walletManager.sendFromOutputs(outs[1], outs[0], destinationSummary[0], destinationSummary[1], destinationSummary[2], changeAddress, automaticCoinHours, burnFactor) } else { - if (addrs.length == 0){ - addrs = stackView.currentItem.advancedPage.getAllAddresses() - + if (addrs[0].length == 0){ + addrs = stackView.currentItem.advancedPage.getAllAddressesWithWallets() } - txn = walletManager.sendFromAddresses(walletSelected, addrs, destinationSummary[0], destinationSummary[1], destinationSummary[2], changeAddress, automaticCoinHours, burnFactor) + txn = walletManager.sendFromAddresses(addrs[1], addrs[0], destinationSummary[0], destinationSummary[1], destinationSummary[2], changeAddress, automaticCoinHours, burnFactor) } - isEncrypted = stackView.currentItem.advancedPage.walletIsEncrypted()[0] + isEncrypted = stackView.currentItem.advancedPage.walletIsEncrypted() } else{ walletSelected = stackView.currentItem.simplePage.getSelectedWallet() isEncrypted = stackView.currentItem.simplePage.walletIsEncrypted() + var addrs = [] + addrs.push([]) + addrs.push([]) + addrs[0].push(stackView.currentItem.simplePage.getDestinationAddress()) + addrs[1].push(walletSelected) txn = walletManager.sendTo(walletSelected, stackView.currentItem.simplePage.getDestinationAddress(), stackView.currentItem.simplePage.getAmount()) } console.log("HT "+txn.hoursTraspassed) - dialogSendTransaction.showPasswordField = isEncrypted// get if the current wallet is encrypted + dialogSendTransaction.showPasswordField = false//isEncrypted// get if the current wallet is encrypted //dialogSendTransaction.previewDate = "2019-02-26 15:27" dialogSendTransaction.previewType = TransactionDetails.Type.Send dialogSendTransaction.previewAmount = txn.amount @@ -70,7 +76,7 @@ Page { dialogSendTransaction.previewtransactionID = txn.transactionId dialogSendTransaction.inputs = txn.inputs dialogSendTransaction.outputs = txn.outputs - dialogSendTransaction.wallet = walletSelected + dialogSendTransaction.walletsAddresses = addrs dialogSendTransaction.open() @@ -168,7 +174,7 @@ Page { DialogSendTransaction { id: dialogSendTransaction anchors.centerIn: Overlay.overlay - property string wallet + property var walletsAddresses readonly property real maxHeight: (expanded ? 490 : 340) + (showPasswordField ? 140 : 0) width: applicationWindow.width > 640 ? 640 - 40 : applicationWindow.width - 40 height: applicationWindow.height > maxHeight ? maxHeight - 40 : applicationWindow.height - 40 @@ -177,8 +183,34 @@ Page { modal: true focus: true onAccepted: { - var signedTxn = walletManager.signTxn(wallet,"", dialogSendTransaction.passwordText, [], txn) - var injected = walletManager.broadcastTxn(signedTxn) + walletManager.signAndBroadcastTxnAsync(walletsAddresses[1], walletsAddresses[0],"", bridgeForPassword, [], txn) + //var signedTxn = walletManager.signTxn(walletsAddresses[1], walletsAddresses[0],"", bridgeForPassword, [], txn) + //var injected = walletManager.broadcastTxn(signedTxn) } } + + DialogGetPassword{ + id: getPasswordDialog + anchors.centerIn: Overlay.overlay + property int nAddress + width: applicationWindow.width > 540 ? 540 - 120 : applicationWindow.width - 40 + height: applicationWindow.height > 570 ? 570 - 180 : applicationWindow.height - 40 + + focus: true + modal: true + onClosed:{ + bridgeForPassword.setResult(getPasswordDialog.password) + bridgeForPassword.unlock() + } + } + + QBridge{ + id: bridgeForPassword + + onGetPassword:{ + getPasswordDialog.title = message + getPasswordDialog.clear() + getPasswordDialog.open() + } + } } diff --git a/src/ui/Settings.qml b/src/ui/Settings.qml index fc193ce2..087abb7c 100644 --- a/src/ui/Settings.qml +++ b/src/ui/Settings.qml @@ -179,4 +179,4 @@ Page { } } } // Dialog -} \ No newline at end of file +} diff --git a/src/ui/SubPageSendAdvanced.qml b/src/ui/SubPageSendAdvanced.qml index 1564dcb2..6a32674a 100644 --- a/src/ui/SubPageSendAdvanced.qml +++ b/src/ui/SubPageSendAdvanced.qml @@ -51,20 +51,28 @@ Page { minFeeAmount = valCH/10 } - function getSelectedAddresses(){ + function getSelectedAddressesWithWallets(){ var indexs = comboBoxWalletsAddressesSendFrom.getCheckedDelegates() var addresses = [] + addresses.push([]) + addresses.push([]) for (var i =0;i< indexs.length; i++){ - addresses.push(comboBoxWalletsAddressesSendFrom.model.addresses[indexs[i]].address) + addresses[0].push(comboBoxWalletsAddressesSendFrom.model.addresses[indexs[i]].address) + addresses[1].push(comboBoxWalletsAddressesSendFrom.model.addresses[indexs[i]].walletId) + //addresses.push(comboBoxWalletsAddressesSendFrom.model.addresses[indexs[i]].address) } return addresses } - function getSelectedOutputs(){ + function getSelectedOutputsWithWallets(){ var indexs = comboBoxWalletsUnspentOutputsSendFrom.getCheckedDelegates() var outputs = [] + outputs.push([]) + outputs.push([]) for (var i =0;i< indexs.length; i++){ - outputs.push(comboBoxWalletsUnspentOutputsSendFrom.model.outputs[indexs[i]].outputID) + outputs[0].push(comboBoxWalletsUnspentOutputsSendFrom.model.outputs[indexs[i]].outputID) + outputs[1].push(comboBoxWalletsUnspentOutputsSendFrom.model.outputs[indexs[i]].walletOwner) + //outputs.push(comboBoxWalletsUnspentOutputsSendFrom.model.outputs[indexs[i]].outputID) } return outputs } @@ -83,7 +91,11 @@ Page { var indexs = comboBoxWalletsSendFrom.getCheckedDelegates() var enc = [] for (var i = 0; i < indexs.length; i++){ - enc.push(comboBoxWalletsSendFrom.model.wallets[indexs[i]].encryptionEnabled) + var walletEncrypted = [] + walletEncrypted.push(comboBoxWalletsSendFrom.model.wallets[indexs[i]].fileName) + walletEncrypted.push(comboBoxWalletsSendFrom.model.wallets[indexs[i]].name) + walletEncrypted.push(comboBoxWalletsSendFrom.model.wallets[indexs[i]].encryptionEnabled) + enc.push(walletEncrypted) } return enc } @@ -111,10 +123,14 @@ Page { return sliderCoinHoursShareFactor.value } - function getAllAddresses(){ + function getAllAddressesWithWallets(){ var addrs = [] + addrs.push([]) + addrs.push([]) for (var i = 0; i < listAddresses.count; i++){ - addrs.push(listAddresses.addresses[i].address) + addrs[0].push(listAddresses.addresses[i].address) + addrs[1].push(listAddresses.addresses[i].walletId) + //addrs.push(listAddresses.addresses[i].address) } return addrs } diff --git a/src/ui/SubPageSendSimple.qml b/src/ui/SubPageSendSimple.qml index 9eba2159..4dd974ff 100644 --- a/src/ui/SubPageSendSimple.qml +++ b/src/ui/SubPageSendSimple.qml @@ -11,6 +11,7 @@ import "Controls" // For quick UI development, switch back to resources when mak Page { id: root property string walletSelected + property string walletSelectedName property bool walletEncrypted: false property string amount property string destinationAddress @@ -24,7 +25,7 @@ Page { return destinationAddress } function walletIsEncrypted(){ - return walletEncrypted + return [walletSelected, walletSelectedName, walletEncrypted] } signal qrCodeRequested(var data) @@ -73,6 +74,7 @@ Page { onActivated: { root.walletSelected = comboBoxWalletsSendFrom.model.wallets[comboBoxWalletsSendFrom.currentIndex].fileName + root.walletSelectedName = comboBoxWalletsSendFrom.model.wallets[comboBoxWalletsSendFrom.currentIndex].name root.walletEncrypted = comboBoxWalletsSendFrom.model.wallets[comboBoxWalletsSendFrom.currentIndex].encryptionEnabled } } // ComboBox diff --git a/src/ui/ToolButtonQR.qml b/src/ui/ToolButtonQR.qml index db5fceec..d1761ead 100644 --- a/src/ui/ToolButtonQR.qml +++ b/src/ui/ToolButtonQR.qml @@ -13,4 +13,4 @@ ToolButton { ToolTip.text: qsTr("Show QR code") ToolTip.visible: hovered // TODO: pressed when mobile? ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval -} \ No newline at end of file +} diff --git a/src/ui/Utils/QRCode.qml b/src/ui/Utils/QRCode.qml index fd4d8d83..82f35494 100644 --- a/src/ui/Utils/QRCode.qml +++ b/src/ui/Utils/QRCode.qml @@ -48,4 +48,4 @@ Canvas { onValueChanged : { requestPaint() } -} \ No newline at end of file +} diff --git a/src/ui/WalletSettings.qml b/src/ui/WalletSettings.qml index 0eace3b3..208f014d 100644 --- a/src/ui/WalletSettings.qml +++ b/src/ui/WalletSettings.qml @@ -41,4 +41,4 @@ ColumnLayout { checked: false } } -} \ No newline at end of file +} diff --git a/src/util/coin.go b/src/util/coin.go index de13f0b4..bf3016d1 100644 --- a/src/util/coin.go +++ b/src/util/coin.go @@ -5,9 +5,10 @@ import ( "github.com/fibercrypto/fibercryptowallet/src/errors" ) -func NewGenericOutput(addr core.Address) GenericOutput { +func NewGenericOutput(addr core.Address, id string) GenericOutput { return GenericOutput{ Address: addr, + id: id, Balance: make(map[string]uint64), } } @@ -16,11 +17,12 @@ func NewGenericOutput(addr core.Address) GenericOutput { type GenericOutput struct { Address core.Address Balance map[string]uint64 + id string } // GetId provides transaction output ID func (gOut *GenericOutput) GetId() string { - return "" + return gOut.id } // IsSpent determines whether there exists a confirmed transaction with an input spending this output diff --git a/src/util/sign.go b/src/util/sign.go index 398e7ea8..2fecebe2 100644 --- a/src/util/sign.go +++ b/src/util/sign.go @@ -50,7 +50,12 @@ func SignTransaction(signerID core.UID, txn core.Transaction, pwd core.PasswordR if signer == nil { return nil, errors.ErrInvalidID } - return signer.SignTransaction(txn, pwd, indices) + // Add signer ID in key value store context + pwdReader := func(msg string, ctx core.KeyValueStore) (string, error) { + newCtx := NewKeyValuesWithDefaults(NewMapWithSingleKey(core.StrSignerID, string(signerID)), ctx) + return pwd(msg, newCtx) + } + return signer.SignTransaction(txn, pwdReader, indices) } // GetSignerDescription human readable caption for signing strategy identified by UID @@ -61,3 +66,58 @@ func GetSignerDescription(signerID core.UID) (string, error) { } return signer.GetSignerDescription(), nil } + +// LookupSignServiceForWallet instantiate signing straegy identified by UID. Fall back to wallet if empty +func LookupSignServiceForWallet(wlt core.Wallet, signerID core.UID) (core.TxnSigner, error) { + if signerID == "" { + wltSigner, isTxnSigner := wlt.(core.TxnSigner) + if !isTxnSigner { + logUtil.WithError(errors.ErrInvalidID).Errorf("Wallet %v can not sign transactions", wlt) + return nil, errors.ErrWalletCantSign + } + return wltSigner, nil + } + if signer := LookupSignService(signerID); signer != nil { + return signer, nil + } + return nil, errors.ErrInvalidID +} + +type signingKeyPair struct { + wallet core.Wallet + signerID core.UID +} + +// GenericMultiWalletSign generic strategy for using multiple wallets to sign a transaction +func GenericMultiWalletSign(txn core.Transaction, signSpec []core.InputSignDescriptor, pwd core.PasswordReader) (signedTxn core.Transaction, err error) { + groups := make(map[signingKeyPair][]string) + // Aggregate inputs by wallet,signer combination + for _, descriptor := range signSpec { + key := signingKeyPair{ + wallet: descriptor.Wallet, + signerID: descriptor.SignerID, + } + inputs, isNotEmpty := groups[key] + if !isNotEmpty { + inputs = []string{} + } + groups[key] = append(inputs, descriptor.InputIndex) + } + signedTxn = txn + + for signPair, indices := range groups { + + signer, err := LookupSignServiceForWallet(signPair.wallet, signPair.signerID) + if err != nil { + logUtil.WithError(err).Errorf("Unknown signer %s specified for signing inputs %v of wallet %v", string(signPair.signerID), indices, signPair.wallet) + return nil, errors.ErrInvalidID + } + signedTxn, err = signPair.wallet.Sign(signedTxn, signer, pwd, indices) + if err != nil { + logUtil.WithError(err).Errorf("Error signing inputs %v of wallet %v with signer %s", indices, signPair.wallet, string(signPair.signerID)) + return nil, err + } + + } + return signedTxn, nil +} diff --git a/src/util/storage.go b/src/util/storage.go index fc494810..110fd85d 100644 --- a/src/util/storage.go +++ b/src/util/storage.go @@ -9,14 +9,17 @@ type KeyValueMap struct { values map[string]interface{} } +// GetValue lookup value for key func (tOpt *KeyValueMap) GetValue(key string) interface{} { return tOpt.values[key] } +// SetValue bind value o known key func (tOpt *KeyValueMap) SetValue(key string, value interface{}) { tOpt.values[key] = value } +// NewKeyValueMap instantiate key value map storage func NewKeyValueMap() *KeyValueMap { tOptions := KeyValueMap{ values: make(map[string]interface{}), @@ -24,7 +27,48 @@ func NewKeyValueMap() *KeyValueMap { return &tOptions } +// NewKeyValueMap instantiate key value map storage +func NewMapWithSingleKey(k string, v interface{}) *KeyValueMap { + store := NewKeyValueMap() + store.values[k] = v + return store +} + +// KeyValuesWithDefaults retrieve and set values of another KeyValueStore with defaults for keys not found +type KeyValuesWithDefaults struct { + values, defaults core.KeyValueStore +} + +// GetValue lookup value for key +func (tOpt *KeyValuesWithDefaults) GetValue(key string) interface{} { + v := tOpt.values.GetValue(key) + if v != nil { + return v + } + return tOpt.defaults.GetValue(key) +} + +// SetValue bind value o known key +func (tOpt *KeyValuesWithDefaults) SetValue(key string, value interface{}) { + tOpt.values.SetValue(key, value) +} + +// NewKeyValuesWithDefaults instantiate key value map storage +func NewKeyValuesWithDefaults(values, defaults core.KeyValueStore) core.KeyValueStore { + if defaults == nil { + return values + } + if values == nil { + values = NewKeyValueMap() + } + tOptions := KeyValuesWithDefaults{ + values: values, + defaults: defaults, + } + return &tOptions +} + // Type assertions var ( - _ core.KeyValueStorage = &KeyValueMap{} + _ core.KeyValueStore = &KeyValueMap{} ) diff --git a/src/util/storage_test.go b/src/util/storage_test.go new file mode 100644 index 00000000..ab6cf1b8 --- /dev/null +++ b/src/util/storage_test.go @@ -0,0 +1,54 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestKeyValuesNoDefaults(t *testing.T) { + m := NewKeyValueMap() + require.Equal(t, m, NewKeyValuesWithDefaults(m, nil)) +} + +func TestKeyValueiWithDefaultsGetValue(t *testing.T) { + // Values override defaults + m1 := NewMapWithSingleKey("k1", "v1") + m2 := NewMapWithSingleKey("k1", "v2") + s := NewKeyValuesWithDefaults(m1, m2) + require.Equal(t, "v1", m1.GetValue("k1")) + require.Equal(t, "v2", m2.GetValue("k1")) + require.Equal(t, "v1", s.GetValue("k1")) + + // Not in values and not in defaults + require.Nil(t, m1.GetValue("z")) + require.Nil(t, m2.GetValue("z")) + require.Nil(t, s.GetValue("z")) + + // Defaults returned if no value found + m2.SetValue("k2", "v3") + require.Nil(t, m1.GetValue("k2")) + require.Equal(t, "v3", m2.GetValue("k2")) + require.Equal(t, "v1", m1.GetValue("k1")) + require.Equal(t, "v2", m2.GetValue("k1")) + require.Nil(t, m1.GetValue("z")) + require.Nil(t, m2.GetValue("z")) + require.Equal(t, "v3", s.GetValue("k2")) + require.Equal(t, "v1", s.GetValue("k1")) + require.Nil(t, s.GetValue("z")) + + // Values returned even if no defaults found + m1.SetValue("k3", "v4") + require.Equal(t, "v4", m1.GetValue("k3")) + require.Nil(t, m2.GetValue("k3")) + require.Nil(t, m1.GetValue("k2")) + require.Equal(t, "v3", m2.GetValue("k2")) + require.Equal(t, "v1", m1.GetValue("k1")) + require.Equal(t, "v2", m2.GetValue("k1")) + require.Nil(t, m1.GetValue("z")) + require.Nil(t, m2.GetValue("z")) + require.Equal(t, "v4", s.GetValue("k3")) + require.Equal(t, "v3", s.GetValue("k2")) + require.Equal(t, "v1", s.GetValue("k1")) + require.Nil(t, s.GetValue("z")) +} diff --git a/src/util/textutil.go b/src/util/textutil.go index aa8970b4..50fac68f 100644 --- a/src/util/textutil.go +++ b/src/util/textutil.go @@ -1,6 +1,17 @@ package util +import ( + "github.com/fibercrypto/fibercryptowallet/src/core" +) + // EmptyPassword read no password -func EmptyPassword(string) (string, error) { +func EmptyPassword(string, core.KeyValueStore) (string, error) { return "", nil } + +// ConstantPassword always return same known password +func ConstantPassword(pwdText string) core.PasswordReader { + return func(string, core.KeyValueStore) (string, error) { + return pwdText, nil + } +} diff --git a/src/util/util.go b/src/util/util.go index c6d600ab..4f735a16 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -1,9 +1,12 @@ package util import ( + "github.com/fibercrypto/fibercryptowallet/src/util/logging" "strconv" ) +var logUtil = logging.MustGetLogger("FiberCrypto util") + func Min(a, b int) int { if a <= b { return a diff --git a/src/util/wallet.go b/src/util/wallet.go new file mode 100644 index 00000000..2504bef3 --- /dev/null +++ b/src/util/wallet.go @@ -0,0 +1,43 @@ +package util + +import ( + "github.com/fibercrypto/fibercryptowallet/src/core" +) + +// SimpleWalletOutput put together transacion output with originating wallet +type SimpleWalletOutput struct { + Wallet core.Wallet + UxOut core.TransactionOutput +} + +// GetWallet return wallet +func (wo *SimpleWalletOutput) GetWallet() core.Wallet { + return wo.Wallet +} + +// GetOutput return transaction output. +func (wo *SimpleWalletOutput) GetOutput() core.TransactionOutput { + return wo.UxOut +} + +// SimpleWalletAddress put together address with owner wallet +type SimpleWalletAddress struct { + Wallet core.Wallet + UxOut core.Address +} + +// GetWallet return wallet +func (wa *SimpleWalletAddress) GetWallet() core.Wallet { + return wa.Wallet +} + +// GetAddress return address +func (wa *SimpleWalletAddress) GetAddress() core.Address { + return wa.UxOut +} + +// Type assertions +var ( + _ core.WalletOutput = &SimpleWalletOutput{} + _ core.WalletAddress = &SimpleWalletAddress{} +) diff --git a/vendor/github.com/skycoin/skycoin/src/wallet/transaction.go b/vendor/github.com/skycoin/skycoin/src/wallet/transaction.go index a92cdccc..f35b68b0 100644 --- a/vendor/github.com/skycoin/skycoin/src/wallet/transaction.go +++ b/vendor/github.com/skycoin/skycoin/src/wallet/transaction.go @@ -140,7 +140,7 @@ func SignTransaction(w Wallet, txn *coin.Transaction, signIndexes []int, uxOuts } if len(toSign) != len(addrs) { - return nil, NewError(errors.New("Wallet cannot sign all requested inputs")) + return nil, NewError(errors.New(fmt.Sprintf("Wallet cannot sign %v %v all requested inputs %s Outs %v", addrs, toSign, w.GetEntries()[0].SkycoinAddress(), uxOuts))) } // Sign the selected inputs