diff --git a/.dockerignore b/.dockerignore index 8e8d2930691c82de499545af58c79784ce15f44b..40c5717ba7f629848b52fc88c6d35eb0e8884b87 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,11 +4,10 @@ LICENSE README.md CHANGELOG.md -images/ docker-compose.yml +images/ tools/ -client/ # Database directory mainnetdb/ diff --git a/.gitignore b/.gitignore index ed554be4bbe94dca5030e830ec94f18fa5ef4953..f8ae258e1db0cc47075a8c7caa709a56a6404e8d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,11 @@ *.dll *.so *.dylib +*.dat +*.dat.bkp + +# cli-wallet config +tools/cli-wallet/config.json # Test binary, build with `go test -c` *.test diff --git a/client/wallet/addressmanager.go b/client/wallet/addressmanager.go new file mode 100644 index 0000000000000000000000000000000000000000..15088cbad7e50ff1f31437563f9ee3275fe50da5 --- /dev/null +++ b/client/wallet/addressmanager.go @@ -0,0 +1,174 @@ +package wallet + +import ( + "runtime" + + walletaddr "github.com/iotaledger/goshimmer/client/wallet/packages/address" + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" + "github.com/iotaledger/hive.go/bitmask" +) + +// AddressManager is an manager struct that allows us to keep track of the used and spent addresses. +type AddressManager struct { + // state of the wallet + seed *walletseed.Seed + lastAddressIndex uint64 + spentAddresses []bitmask.BitMask + + // internal variables for faster access + firstUnspentAddressIndex uint64 + lastUnspentAddressIndex uint64 +} + +// NewAddressManager is the constructor for the AddressManager type. +func NewAddressManager(seed *walletseed.Seed, lastAddressIndex uint64, spentAddresses []bitmask.BitMask) (addressManager *AddressManager) { + defer runtime.KeepAlive(spentAddresses) + + addressManager = &AddressManager{ + seed: seed, + lastAddressIndex: lastAddressIndex, + spentAddresses: spentAddresses, + } + addressManager.updateFirstUnspentAddressIndex() + addressManager.updateLastUnspentAddressIndex() + + return +} + +// Address returns the address that belongs to the given index. +func (addressManager *AddressManager) Address(addressIndex uint64) walletaddr.Address { + // update lastUnspentAddressIndex if necessary + addressManager.spentAddressIndexes(addressIndex) + + return addressManager.seed.Address(addressIndex) +} + +// Addresses returns a list of all addresses of the wallet. +func (addressManager *AddressManager) Addresses() (addresses []walletaddr.Address) { + addresses = make([]walletaddr.Address, addressManager.lastAddressIndex+1) + for i := uint64(0); i <= addressManager.lastAddressIndex; i++ { + addresses[i] = addressManager.Address(i) + } + + return +} + +// UnspentAddresses returns a list of all unspent addresses of the wallet. +func (addressManager *AddressManager) UnspentAddresses() (addresses []walletaddr.Address) { + addresses = make([]walletaddr.Address, 0) + for i := addressManager.firstUnspentAddressIndex; i <= addressManager.lastAddressIndex; i++ { + if !addressManager.IsAddressSpent(i) { + addresses = append(addresses, addressManager.Address(i)) + } + } + + return +} + +// SpentAddresses returns a list of all spent addresses of the wallet. +func (addressManager *AddressManager) SpentAddresses() (addresses []walletaddr.Address) { + addresses = make([]walletaddr.Address, 0) + for i := uint64(0); i <= addressManager.lastAddressIndex; i++ { + if addressManager.IsAddressSpent(i) { + addresses = append(addresses, addressManager.Address(i)) + } + } + + return +} + +// FirstUnspentAddress returns the first unspent address that we know. +func (addressManager *AddressManager) FirstUnspentAddress() walletaddr.Address { + return addressManager.Address(addressManager.firstUnspentAddressIndex) +} + +// LastUnspentAddress returns the last unspent address that we know. +func (addressManager *AddressManager) LastUnspentAddress() walletaddr.Address { + return addressManager.Address(addressManager.lastUnspentAddressIndex) +} + +// NewAddress generates and returns a new unused address. +func (addressManager *AddressManager) NewAddress() walletaddr.Address { + return addressManager.Address(addressManager.lastAddressIndex + 1) +} + +// MarkAddressSpent marks the given address as spent. +func (addressManager *AddressManager) MarkAddressSpent(addressIndex uint64) { + // determine indexes + sliceIndex, bitIndex := addressManager.spentAddressIndexes(addressIndex) + + // mark address as spent + addressManager.spentAddresses[sliceIndex] = addressManager.spentAddresses[sliceIndex].SetFlag(uint(bitIndex)) + + // update spent address indexes + if addressIndex == addressManager.firstUnspentAddressIndex { + addressManager.updateFirstUnspentAddressIndex() + } + if addressIndex == addressManager.lastUnspentAddressIndex { + addressManager.updateLastUnspentAddressIndex() + } +} + +// IsAddressSpent returns true if the address given by the address index was spent already. +func (addressManager *AddressManager) IsAddressSpent(addressIndex uint64) bool { + sliceIndex, bitIndex := addressManager.spentAddressIndexes(addressIndex) + + return addressManager.spentAddresses[sliceIndex].HasFlag(uint(bitIndex)) +} + +// spentAddressIndexes retrieves the indexes for the internal representation of the spend addresses bitmask slice that +// belongs to the given address index. It automatically increases the capacity and updates the lastAddressIndex and the +// lastUnspentAddressIndex if a new address is generated for the first time. +func (addressManager *AddressManager) spentAddressIndexes(addressIndex uint64) (sliceIndex uint64, bitIndex uint64) { + // calculate result + spentAddressesCapacity := uint64(len(addressManager.spentAddresses)) + sliceIndex = addressIndex / 8 + bitIndex = addressIndex % 8 + + // extend capacity to make space for the requested index + if sliceIndex+1 > spentAddressesCapacity { + addressManager.spentAddresses = append(addressManager.spentAddresses, make([]bitmask.BitMask, sliceIndex-spentAddressesCapacity+1)...) + } + + // update lastAddressIndex if the index is bigger + if addressIndex > addressManager.lastAddressIndex { + addressManager.lastAddressIndex = addressIndex + } + + // update lastUnspentAddressIndex if necessary + if addressIndex > addressManager.lastUnspentAddressIndex && !addressManager.spentAddresses[sliceIndex].HasFlag(uint(bitIndex)) { + addressManager.lastUnspentAddressIndex = addressIndex + } + + return +} + +// updateFirstUnspentAddressIndex searches for the first unspent address and updates the firstUnspentAddressIndex. +func (addressManager *AddressManager) updateFirstUnspentAddressIndex() { + for i := addressManager.firstUnspentAddressIndex; true; i++ { + if !addressManager.IsAddressSpent(i) { + addressManager.firstUnspentAddressIndex = i + + return + } + } +} + +// updateLastUnspentAddressIndex searches for the first unspent address and updates the lastUnspentAddressIndex. +func (addressManager *AddressManager) updateLastUnspentAddressIndex() { + // search for last unspent address + for i := addressManager.lastUnspentAddressIndex; true; i-- { + if !addressManager.IsAddressSpent(i) { + addressManager.lastUnspentAddressIndex = i + + return + } + + if i == 0 { + break + } + } + + // or generate a new unspent address + addressManager.NewAddress() +} diff --git a/client/wallet/asset.go b/client/wallet/asset.go new file mode 100644 index 0000000000000000000000000000000000000000..c3990e81616bf43bba6460ed7e5cafe29f0b5ee9 --- /dev/null +++ b/client/wallet/asset.go @@ -0,0 +1,27 @@ +package wallet + +import ( + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" +) + +// Asset represents a container for all the information regarding a colored coin. +type Asset struct { + // Color contains the identifier of this asset + Color balance.Color + + // Name of the asset + Name string + + // currency symbol of the asset (optional) + Symbol string + + // Precision defines how many decimal places are shown when showing this asset in wallets + Precision int + + // Address defines the target address where the asset is supposed to be created + Address address.Address + + // the amount of tokens that we want to create + Amount uint64 +} diff --git a/client/wallet/assetregistry.go b/client/wallet/assetregistry.go new file mode 100644 index 0000000000000000000000000000000000000000..aee5cdeedf863f1f1e8e39d5151ca1718ead9a9c --- /dev/null +++ b/client/wallet/assetregistry.go @@ -0,0 +1,158 @@ +package wallet + +import ( + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/hive.go/marshalutil" + "github.com/iotaledger/hive.go/typeutils" +) + +// AssetRegistry represents a registry for colored coins, that stores the relevant metadata in a dictionary. +type AssetRegistry struct { + assets map[balance.Color]Asset +} + +// NewAssetRegistry is the constructor for the AssetRegistry. +func NewAssetRegistry() *AssetRegistry { + return &AssetRegistry{ + make(map[balance.Color]Asset), + } +} + +// ParseAssetRegistry is a utility function that can be used to parse a marshaled version of the registry. +func ParseAssetRegistry(marshalUtil *marshalutil.MarshalUtil) (assetRegistry *AssetRegistry, consumedBytes int, err error) { + assetRegistry = &AssetRegistry{ + assets: make(map[balance.Color]Asset), + } + + startingOffset := marshalUtil.ReadOffset() + + assetCount, err := marshalUtil.ReadUint64() + if err != nil { + return + } + + for i := uint64(0); i < assetCount; i++ { + asset := Asset{} + + colorBytes, parseErr := marshalUtil.ReadBytes(balance.ColorLength) + if parseErr != nil { + err = parseErr + + return + } + color, _, parseErr := balance.ColorFromBytes(colorBytes) + if parseErr != nil { + err = parseErr + + return + } + + nameLength, parseErr := marshalUtil.ReadUint32() + if parseErr != nil { + err = parseErr + + return + } + nameBytes, parseErr := marshalUtil.ReadBytes(int(nameLength)) + if parseErr != nil { + err = parseErr + + return + } + + symbolLength, parseErr := marshalUtil.ReadUint32() + if parseErr != nil { + err = parseErr + + return + } + symbolBytes, parseErr := marshalUtil.ReadBytes(int(symbolLength)) + if parseErr != nil { + err = parseErr + + return + } + + precision, parseErr := marshalUtil.ReadUint32() + if parseErr != nil { + err = parseErr + + return + } + + asset.Color = color + asset.Name = typeutils.BytesToString(nameBytes) + asset.Symbol = typeutils.BytesToString(symbolBytes) + asset.Precision = int(precision) + + assetRegistry.assets[color] = asset + } + + consumedBytes = marshalUtil.ReadOffset() - startingOffset + + return +} + +// RegisterAsset registers an asset in the registry, so we can look up names and symbol of colored coins. +func (assetRegistry *AssetRegistry) RegisterAsset(color balance.Color, asset Asset) { + assetRegistry.assets[color] = asset +} + +// Name returns the name of the given asset. +func (assetRegistry *AssetRegistry) Name(color balance.Color) string { + if asset, assetExists := assetRegistry.assets[color]; assetExists { + return asset.Name + } + + if color == balance.ColorIOTA { + return "IOTA" + } + + return color.String() +} + +// Symbol return the symbol of the token. +func (assetRegistry *AssetRegistry) Symbol(color balance.Color) string { + if asset, assetExists := assetRegistry.assets[color]; assetExists { + return asset.Symbol + } + + if color == balance.ColorIOTA { + return "I" + } + + return "cI" +} + +// Precision returns the amount of decimal places that this token uses. +func (assetRegistry *AssetRegistry) Precision(color balance.Color) int { + if asset, assetExists := assetRegistry.assets[color]; assetExists { + return asset.Precision + } + + return 0 +} + +// Bytes marshal the registry into a sequence of bytes. +func (assetRegistry *AssetRegistry) Bytes() []byte { + marshalUtil := marshalutil.New() + + assetCount := len(assetRegistry.assets) + marshalUtil.WriteUint64(uint64(assetCount)) + + for color, asset := range assetRegistry.assets { + marshalUtil.WriteBytes(color.Bytes()) + + nameBytes := typeutils.StringToBytes(asset.Name) + marshalUtil.WriteUint32(uint32(len(nameBytes))) + marshalUtil.WriteBytes(nameBytes) + + symbolBytes := typeutils.StringToBytes(asset.Symbol) + marshalUtil.WriteUint32(uint32(len(symbolBytes))) + marshalUtil.WriteBytes(symbolBytes) + + marshalUtil.WriteUint32(uint32(asset.Precision)) + } + + return marshalUtil.Bytes() +} diff --git a/client/wallet/connector.go b/client/wallet/connector.go new file mode 100644 index 0000000000000000000000000000000000000000..c14ef999697476a4f46eb565c3fd6da8093ba5af --- /dev/null +++ b/client/wallet/connector.go @@ -0,0 +1,14 @@ +package wallet + +import ( + walletaddr "github.com/iotaledger/goshimmer/client/wallet/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" +) + +// Connector represents an interface that defines how the wallet interacts with the network. A wallet can either be used +// locally on a server or it can connect remotely using the web API. +type Connector interface { + UnspentOutputs(addresses ...walletaddr.Address) (unspentOutputs map[walletaddr.Address]map[transaction.ID]*Output, err error) + SendTransaction(tx *transaction.Transaction) (err error) + RequestFaucetFunds(addr walletaddr.Address) (err error) +} diff --git a/client/wallet/options.go b/client/wallet/options.go new file mode 100644 index 0000000000000000000000000000000000000000..1fd746dcfc98df88025b6d026165e7913b1e9e38 --- /dev/null +++ b/client/wallet/options.go @@ -0,0 +1,42 @@ +package wallet + +import ( + "net/http" + + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" + "github.com/iotaledger/hive.go/bitmask" +) + +// Option represents an optional parameter . +type Option func(*Wallet) + +// WebAPI connects the wallet with the remote API of a node. +func WebAPI(baseURL string, httpClient ...http.Client) Option { + return func(wallet *Wallet) { + wallet.connector = NewWebConnector(baseURL, httpClient...) + } +} + +// Import restores a wallet that has previously been created. +func Import(seed *walletseed.Seed, lastAddressIndex uint64, spentAddresses []bitmask.BitMask, assetRegistry *AssetRegistry) Option { + return func(wallet *Wallet) { + wallet.addressManager = NewAddressManager(seed, lastAddressIndex, spentAddresses) + wallet.assetRegistry = assetRegistry + } +} + +// ReusableAddress configures the wallet to run in "single address" mode where all the funds are always managed on a +// single reusable address. +func ReusableAddress(enabled bool) Option { + return func(wallet *Wallet) { + wallet.reusableAddress = enabled + } +} + +// GenericConnector allows us to provide a generic connector to the wallet. It can be used to mock the behavior of a +// real connector in tests or to provide new connection methods for nodes. +func GenericConnector(connector Connector) Option { + return func(wallet *Wallet) { + wallet.connector = connector + } +} diff --git a/client/wallet/output.go b/client/wallet/output.go new file mode 100644 index 0000000000000000000000000000000000000000..4000677812c0c59702bb10565174761122881971 --- /dev/null +++ b/client/wallet/output.go @@ -0,0 +1,24 @@ +package wallet + +import ( + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" +) + +// Output is a wallet specific representation of an output in the IOTA network. +type Output struct { + Address address.Address + TransactionID transaction.ID + Balances map[balance.Color]uint64 + InclusionState InclusionState +} + +// InclusionState is a container for the different flags of an output that define if it was accepted in the network. +type InclusionState struct { + Liked bool + Confirmed bool + Rejected bool + Conflicting bool + Spent bool +} diff --git a/client/wallet/packages/address/address.go b/client/wallet/packages/address/address.go new file mode 100644 index 0000000000000000000000000000000000000000..1719169f85129b4aad462e2a10ec64abfb607f44 --- /dev/null +++ b/client/wallet/packages/address/address.go @@ -0,0 +1,15 @@ +package address + +import ( + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" +) + +// Address represents an address in a wallet. It extends the normal address type with an index number that was used to +// generate the address from its seed. +type Address struct { + address.Address + Index uint64 +} + +// AddressEmpty represents the 0-value of an address. +var AddressEmpty = Address{} diff --git a/dapps/valuetransfers/packages/wallet/seed.go b/client/wallet/packages/seed/seed.go similarity index 61% rename from dapps/valuetransfers/packages/wallet/seed.go rename to client/wallet/packages/seed/seed.go index 9e94c3d79eb7a4ed7dffc10c0541767aeececf15..3939464755c09fed4339dffa883d890c210897e1 100644 --- a/dapps/valuetransfers/packages/wallet/seed.go +++ b/client/wallet/packages/seed/seed.go @@ -1,8 +1,9 @@ -package wallet +package seed import ( "github.com/iotaledger/hive.go/crypto/ed25519" + walletaddr "github.com/iotaledger/goshimmer/client/wallet/packages/address" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" ) @@ -20,7 +21,10 @@ func NewSeed(optionalSeedBytes ...[]byte) *Seed { } } -// Address returns an ed25519 address which can be used for receiving or sending funds. -func (seed *Seed) Address(index uint64) address.Address { - return address.FromED25519PubKey(seed.Seed.KeyPair(index).PublicKey) +// Address returns an Address which can be used for receiving or sending funds. +func (seed *Seed) Address(index uint64) walletaddr.Address { + return walletaddr.Address{ + Address: address.FromED25519PubKey(seed.Seed.KeyPair(index).PublicKey), + Index: index, + } } diff --git a/client/wallet/sendfunds_options.go b/client/wallet/sendfunds_options.go new file mode 100644 index 0000000000000000000000000000000000000000..229919a0282b75d20794c977e6e83c627b272e63 --- /dev/null +++ b/client/wallet/sendfunds_options.go @@ -0,0 +1,100 @@ +package wallet + +import ( + "errors" + + walletaddr "github.com/iotaledger/goshimmer/client/wallet/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" +) + +// SendFundsOption is the type for the optional parameters for the SendFunds call. +type SendFundsOption func(*sendFundsOptions) error + +// Destination is an option for the SendFunds call that defines a destination for funds that are supposed to be moved. +func Destination(addr address.Address, amount uint64, optionalColor ...balance.Color) SendFundsOption { + // determine optional output color + var outputColor balance.Color + switch len(optionalColor) { + case 0: + outputColor = balance.ColorIOTA + case 1: + outputColor = optionalColor[0] + default: + return optionError(errors.New("providing more than one output color for the destination of funds is forbidden")) + } + + // return an error if the amount is less + if amount == 0 { + return optionError(errors.New("the amount provided in the destinations needs to be larger than 0")) + } + + // return Option + return func(options *sendFundsOptions) error { + // initialize destinations property + if options.Destinations == nil { + options.Destinations = make(map[address.Address]map[balance.Color]uint64) + } + + // initialize address specific destination + if _, addressExists := options.Destinations[addr]; !addressExists { + options.Destinations[addr] = make(map[balance.Color]uint64) + } + + // initialize color specific destination + if _, colorExists := options.Destinations[addr][outputColor]; !colorExists { + options.Destinations[addr][outputColor] = 0 + } + + // increase amount + options.Destinations[addr][outputColor] += amount + + return nil + } +} + +// Remainder is an option for the SendsFunds call that allows us to specify the remainder address that is +// supposed to be used in the corresponding transaction. +func Remainder(addr walletaddr.Address) SendFundsOption { + return func(options *sendFundsOptions) error { + options.RemainderAddress = addr + + return nil + } +} + +// sendFundsOptions is a struct that is used to aggregate the optional parameters provided in the SendFunds call. +type sendFundsOptions struct { + Destinations map[address.Address]map[balance.Color]uint64 + RemainderAddress walletaddr.Address +} + +// buildSendFundsOptions is a utility function that constructs the sendFundsOptions. +func buildSendFundsOptions(options ...SendFundsOption) (result *sendFundsOptions, err error) { + // create options to collect the arguments provided + result = &sendFundsOptions{} + + // apply arguments to our options + for _, option := range options { + if err = option(result); err != nil { + return + } + } + + // sanitize parameters + if len(result.Destinations) == 0 { + err = errors.New("you need to provide at least one Destination for a valid transfer to be issued") + + return + } + + return +} + +// optionError is a utility function that returns a Option that returns the error provided in the +// argument. +func optionError(err error) SendFundsOption { + return func(options *sendFundsOptions) error { + return err + } +} diff --git a/client/wallet/serverstatus.go b/client/wallet/serverstatus.go new file mode 100644 index 0000000000000000000000000000000000000000..fce53b852a599075870a7fb880837d942e86914c --- /dev/null +++ b/client/wallet/serverstatus.go @@ -0,0 +1,8 @@ +package wallet + +// ServerStatus defines the information of connected server +type ServerStatus struct { + ID string + Synced bool + Version string +} diff --git a/client/wallet/unspentoutputmanager.go b/client/wallet/unspentoutputmanager.go new file mode 100644 index 0000000000000000000000000000000000000000..3d54c6daeafdf0904ec92f46931b47dc60cd62eb --- /dev/null +++ b/client/wallet/unspentoutputmanager.go @@ -0,0 +1,112 @@ +package wallet + +import ( + walletaddr "github.com/iotaledger/goshimmer/client/wallet/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" +) + +// UnspentOutputManager is a manager for the unspent outputs of the addresses of a wallet. It allows us to keep track of +// the spent state of outputs using our local knowledge about outputs that have already been spent and allows us to +// cache results that would otherwise have to be requested by the server over and over again. +type UnspentOutputManager struct { + addressManager *AddressManager + connector Connector + unspentOutputs map[walletaddr.Address]map[transaction.ID]*Output +} + +// NewUnspentOutputManager creates a new UnspentOutputManager. +func NewUnspentOutputManager(addressManager *AddressManager, connector Connector) (outputManager *UnspentOutputManager) { + outputManager = &UnspentOutputManager{ + addressManager: addressManager, + connector: connector, + unspentOutputs: make(map[walletaddr.Address]map[transaction.ID]*Output), + } + + outputManager.Refresh(true) + + return +} + +// Refresh retrieves the unspent outputs from the node. If includeSpentAddresses is set to true, then it also scans the +// addresses from which we previously spent already. +func (unspentOutputManager *UnspentOutputManager) Refresh(includeSpentAddresses ...bool) (err error) { + var addressesToRefresh []walletaddr.Address + if len(includeSpentAddresses) >= 1 && includeSpentAddresses[0] { + addressesToRefresh = unspentOutputManager.addressManager.Addresses() + } else { + addressesToRefresh = unspentOutputManager.addressManager.UnspentAddresses() + } + + unspentOutputs, err := unspentOutputManager.connector.UnspentOutputs(addressesToRefresh...) + if err != nil { + return + } + + for addr, unspentOutputs := range unspentOutputs { + for transactionID, output := range unspentOutputs { + if _, addressExists := unspentOutputManager.unspentOutputs[addr]; !addressExists { + unspentOutputManager.unspentOutputs[addr] = make(map[transaction.ID]*Output) + } + + // mark the output as spent if we already marked it as spent locally + if existingOutput, outputExists := unspentOutputManager.unspentOutputs[addr][transactionID]; outputExists && existingOutput.InclusionState.Spent { + output.InclusionState.Spent = true + } + + unspentOutputManager.unspentOutputs[addr][transactionID] = output + } + } + + return +} + +// UnspentOutputs returns the outputs that have not been spent, yet. +func (unspentOutputManager *UnspentOutputManager) UnspentOutputs(addresses ...walletaddr.Address) (unspentOutputs map[walletaddr.Address]map[transaction.ID]*Output) { + // prepare result + unspentOutputs = make(map[walletaddr.Address]map[transaction.ID]*Output) + + // retrieve the list of addresses from the address manager if none was provided + if len(addresses) == 0 { + addresses = unspentOutputManager.addressManager.Addresses() + } + + // iterate through addresses and scan for unspent outputs + for _, addr := range addresses { + // skip the address if we have no outputs for it stored + unspentOutputsOnAddress, addressExistsInStoredOutputs := unspentOutputManager.unspentOutputs[addr] + if !addressExistsInStoredOutputs { + continue + } + + // iterate through outputs + for transactionID, output := range unspentOutputsOnAddress { + // skip spent outputs + if output.InclusionState.Spent { + continue + } + + // store unspent outputs in result + if _, addressExists := unspentOutputs[addr]; !addressExists { + unspentOutputs[addr] = make(map[transaction.ID]*Output) + } + unspentOutputs[addr][transactionID] = output + } + } + + return +} + +// MarkOutputSpent marks the output identified by the given parameters as spent. +func (unspentOutputManager *UnspentOutputManager) MarkOutputSpent(addr walletaddr.Address, transactionID transaction.ID) { + // abort if we try to mark an unknown output as spent + if _, addressExists := unspentOutputManager.unspentOutputs[addr]; !addressExists { + return + } + output, outputExists := unspentOutputManager.unspentOutputs[addr][transactionID] + if !outputExists { + return + } + + // mark output as spent + output.InclusionState.Spent = true +} diff --git a/client/wallet/wallet.go b/client/wallet/wallet.go new file mode 100644 index 0000000000000000000000000000000000000000..533677fec95c2a4bc1ec5f2bd37bf690742f501f --- /dev/null +++ b/client/wallet/wallet.go @@ -0,0 +1,428 @@ +package wallet + +import ( + "errors" + "reflect" + "time" + "unsafe" + + walletaddr "github.com/iotaledger/goshimmer/client/wallet/packages/address" + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address/signaturescheme" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/hive.go/bitmask" + "github.com/iotaledger/hive.go/marshalutil" +) + +// Wallet represents a simple cryptocurrency wallet for the IOTA tangle. It contains the logic to manage the movement of +// funds. +type Wallet struct { + addressManager *AddressManager + assetRegistry *AssetRegistry + unspentOutputManager *UnspentOutputManager + connector Connector + + // if this option is enabled the wallet will use a single reusable address instead of changing addresses. + reusableAddress bool +} + +// New is the factory method of the wallet. It either creates a new wallet or restores the wallet backup that is handed +// in as an optional parameter. +func New(options ...Option) (wallet *Wallet) { + // create wallet + wallet = &Wallet{ + assetRegistry: NewAssetRegistry(), + } + + // configure wallet + for _, option := range options { + option(wallet) + } + + // initialize wallet with default address manager if we did not import a previous wallet + if wallet.addressManager == nil { + wallet.addressManager = NewAddressManager(walletseed.NewSeed(), 0, []bitmask.BitMask{}) + } + + // initialize asset registry if none was provided in the options. + if wallet.assetRegistry == nil { + wallet.assetRegistry = NewAssetRegistry() + } + + // initialize wallet with default connector (server) if none was provided + if wallet.connector == nil { + panic("you need to provide a connector for your wallet") + } + + // initialize output manager + wallet.unspentOutputManager = NewUnspentOutputManager(wallet.addressManager, wallet.connector) + err := wallet.unspentOutputManager.Refresh(true) + if err != nil { + panic(err) + } + + return +} + +// ServerStatus retrieves the connected server status. +func (wallet *Wallet) ServerStatus() (status ServerStatus, err error) { + return wallet.connector.(*WebConnector).ServerStatus() +} + +// SendFunds issues a payment of the given amount to the given address. +func (wallet *Wallet) SendFunds(options ...SendFundsOption) (tx *transaction.Transaction, err error) { + // build options from the parameters + sendFundsOptions, err := buildSendFundsOptions(options...) + if err != nil { + return + } + + // determine which outputs to use for our transfer + consumedOutputs, err := wallet.determineOutputsToConsume(sendFundsOptions) + if err != nil { + return + } + + // build transaction + inputs, consumedFunds := wallet.buildInputs(consumedOutputs) + outputs := wallet.buildOutputs(sendFundsOptions, consumedFunds) + tx = transaction.New(inputs, outputs) + for addr := range consumedOutputs { + tx.Sign(signaturescheme.ED25519(*wallet.Seed().KeyPair(addr.Index))) + } + + // mark outputs as spent + for addr, outputs := range consumedOutputs { + for transactionID := range outputs { + wallet.unspentOutputManager.MarkOutputSpent(addr, transactionID) + } + } + + // mark addresses as spent + if !wallet.reusableAddress { + for addr := range consumedOutputs { + wallet.addressManager.MarkAddressSpent(addr.Index) + } + } + + // send transaction + err = wallet.connector.SendTransaction(tx) + + return +} + +// CreateAsset creates a new colored token with the given details. +func (wallet *Wallet) CreateAsset(asset Asset) (assetColor balance.Color, err error) { + if asset.Amount == 0 { + err = errors.New("required to provide the amount when trying to create an asset") + + return + } + + if asset.Name == "" { + err = errors.New("required to provide a name when trying to create an asset") + + return + } + + tx, err := wallet.SendFunds( + Destination(wallet.ReceiveAddress().Address, asset.Amount, balance.ColorNew), + ) + if err != nil { + return + } + + assetColor, _, err = balance.ColorFromBytes(tx.ID().Bytes()) + if err != nil { + return + } + + wallet.assetRegistry.RegisterAsset(assetColor, asset) + + return +} + +// AssetRegistry return the internal AssetRegistry instance of the wallet. +func (wallet *Wallet) AssetRegistry() *AssetRegistry { + return wallet.assetRegistry +} + +// ReceiveAddress returns the last receive address of the wallet. +func (wallet *Wallet) ReceiveAddress() walletaddr.Address { + return wallet.addressManager.LastUnspentAddress() +} + +// NewReceiveAddress generates and returns a new unused receive address. +func (wallet *Wallet) NewReceiveAddress() walletaddr.Address { + return wallet.addressManager.NewAddress() +} + +// RemainderAddress returns the address that is used for the remainder of funds. +func (wallet *Wallet) RemainderAddress() walletaddr.Address { + return wallet.addressManager.FirstUnspentAddress() +} + +// UnspentOutputs returns the unspent outputs that are available for spending. +func (wallet *Wallet) UnspentOutputs() map[walletaddr.Address]map[transaction.ID]*Output { + return wallet.unspentOutputManager.UnspentOutputs() +} + +// RequestFaucetFunds requests some funds from the faucet for testing purposes. +func (wallet *Wallet) RequestFaucetFunds(waitForConfirmation ...bool) (err error) { + if len(waitForConfirmation) == 0 || !waitForConfirmation[0] { + err = wallet.connector.RequestFaucetFunds(wallet.ReceiveAddress()) + + return + } + + if err = wallet.Refresh(); err != nil { + return + } + confirmedBalance, _, err := wallet.Balance() + if err != nil { + return + } + + err = wallet.connector.RequestFaucetFunds(wallet.ReceiveAddress()) + if err != nil { + return + } + + for { + time.Sleep(500 * time.Millisecond) + + if err = wallet.Refresh(); err != nil { + return + } + newConfirmedBalance, _, balanceErr := wallet.Balance() + if balanceErr != nil { + err = balanceErr + + return + } + + if !reflect.DeepEqual(confirmedBalance, newConfirmedBalance) { + return + } + } +} + +// Refresh scans the addresses for incoming transactions. If the optional rescanSpentAddresses parameter is set to true +// we also scan the spent addresses again (this can take longer). +func (wallet *Wallet) Refresh(rescanSpentAddresses ...bool) (err error) { + err = wallet.unspentOutputManager.Refresh(rescanSpentAddresses...) + + return +} + +// Balance returns the confirmed and pending balance of the funds managed by this wallet. +func (wallet *Wallet) Balance() (confirmedBalance map[balance.Color]uint64, pendingBalance map[balance.Color]uint64, err error) { + err = wallet.unspentOutputManager.Refresh() + if err != nil { + return + } + + confirmedBalance = make(map[balance.Color]uint64) + pendingBalance = make(map[balance.Color]uint64) + + // iterate through the unspent outputs + for _, outputsOnAddress := range wallet.unspentOutputManager.UnspentOutputs() { + for _, output := range outputsOnAddress { + // skip if the output was rejected or spent already + if output.InclusionState.Spent || output.InclusionState.Rejected { + continue + } + + // determine target map + var targetMap map[balance.Color]uint64 + if output.InclusionState.Confirmed { + targetMap = confirmedBalance + } else { + targetMap = pendingBalance + } + + // store amount + for color, amount := range output.Balances { + targetMap[color] += amount + } + } + } + + return +} + +// Seed returns the seed of this wallet that is used to generate all of the wallets addresses and private keys. +func (wallet *Wallet) Seed() *walletseed.Seed { + return wallet.addressManager.seed +} + +// AddressManager returns the manager for the addresses of this wallet. +func (wallet *Wallet) AddressManager() *AddressManager { + return wallet.addressManager +} + +// ExportState exports the current state of the wallet to a marshaled version. +func (wallet *Wallet) ExportState() []byte { + marshalUtil := marshalutil.New() + marshalUtil.WriteBytes(wallet.Seed().Bytes()) + marshalUtil.WriteUint64(wallet.AddressManager().lastAddressIndex) + marshalUtil.WriteBytes(wallet.assetRegistry.Bytes()) + marshalUtil.WriteBytes(*(*[]byte)(unsafe.Pointer(&wallet.addressManager.spentAddresses))) + + return marshalUtil.Bytes() +} + +func (wallet *Wallet) determineOutputsToConsume(sendFundsOptions *sendFundsOptions) (outputsToConsume map[walletaddr.Address]map[transaction.ID]*Output, err error) { + // initialize return values + outputsToConsume = make(map[walletaddr.Address]map[transaction.ID]*Output) + + // aggregate total amount of required funds, so we now what and how many funds we need + requiredFunds := make(map[balance.Color]uint64) + for _, coloredBalances := range sendFundsOptions.Destinations { + for color, amount := range coloredBalances { + // if we want to color sth then we need fresh IOTA + if color == balance.ColorNew { + color = balance.ColorIOTA + } + + requiredFunds[color] += amount + } + } + + // refresh balances so we get the latest changes + if err = wallet.unspentOutputManager.Refresh(); err != nil { + return + } + + // look for the required funds in the available unspent outputs + for addr, unspentOutputsOnAddress := range wallet.unspentOutputManager.UnspentOutputs() { + // keeps track if outputs from this address are supposed to be spent + outputsFromAddressSpent := false + + // scan the outputs on this address for required funds + for transactionID, output := range unspentOutputsOnAddress { + // keeps track if the output contains any usable funds + requiredColorFoundInOutput := false + + // subtract the found matching balances from the required funds + for color, availableBalance := range output.Balances { + if requiredAmount, requiredColorExists := requiredFunds[color]; requiredColorExists { + if requiredAmount > availableBalance { + requiredFunds[color] -= availableBalance + } else { + delete(requiredFunds, color) + } + + requiredColorFoundInOutput = true + } + } + + // if we found required tokens in this output + if requiredColorFoundInOutput { + // store the output in the outputs to use for the transfer + if _, addressEntryExists := outputsToConsume[addr]; !addressEntryExists { + outputsToConsume[addr] = make(map[transaction.ID]*Output) + } + outputsToConsume[addr][transactionID] = output + + // mark address as spent + outputsFromAddressSpent = true + } + } + + // if outputs from this address were spent add the remaining outputs as well (we want to spend only once from + // every address if we are not using a reusable address) + if !wallet.reusableAddress && outputsFromAddressSpent { + for transactionID, output := range unspentOutputsOnAddress { + outputsToConsume[addr][transactionID] = output + } + } + } + + // update remainder address with default value (first unspent address) if none was provided + if sendFundsOptions.RemainderAddress.Address == address.Empty { + sendFundsOptions.RemainderAddress = wallet.RemainderAddress() + } + if _, remainderAddressInConsumedOutputs := outputsToConsume[sendFundsOptions.RemainderAddress]; remainderAddressInConsumedOutputs && !wallet.reusableAddress { + sendFundsOptions.RemainderAddress = wallet.ReceiveAddress() + } + if _, remainderAddressInConsumedOutputs := outputsToConsume[sendFundsOptions.RemainderAddress]; remainderAddressInConsumedOutputs && !wallet.reusableAddress { + sendFundsOptions.RemainderAddress = wallet.NewReceiveAddress() + } + + // check if we have found all required funds + if len(requiredFunds) != 0 { + outputsToConsume = nil + err = errors.New("not enough funds to create transaction") + } + + return +} + +func (wallet *Wallet) buildInputs(outputsToUseAsInputs map[walletaddr.Address]map[transaction.ID]*Output) (inputs *transaction.Inputs, consumedFunds map[balance.Color]uint64) { + consumedInputs := make([]transaction.OutputID, 0) + consumedFunds = make(map[balance.Color]uint64) + for addr, unspentOutputsOfAddress := range outputsToUseAsInputs { + for transactionID, output := range unspentOutputsOfAddress { + consumedInputs = append(consumedInputs, transaction.NewOutputID(addr.Address, transactionID)) + + for color, amount := range output.Balances { + consumedFunds[color] += amount + } + } + } + inputs = transaction.NewInputs(consumedInputs...) + + return +} + +func (wallet *Wallet) buildOutputs(sendFundsOptions *sendFundsOptions, consumedFunds map[balance.Color]uint64) (outputs *transaction.Outputs) { + // build outputs for destinations + outputsByColor := make(map[address.Address]map[balance.Color]uint64) + for walletAddress, coloredBalances := range sendFundsOptions.Destinations { + if _, addressExists := outputsByColor[walletAddress]; !addressExists { + outputsByColor[walletAddress] = make(map[balance.Color]uint64) + } + for color, amount := range coloredBalances { + outputsByColor[walletAddress][color] += amount + if color == balance.ColorNew { + consumedFunds[balance.ColorIOTA] -= amount + + if consumedFunds[balance.ColorIOTA] == 0 { + delete(consumedFunds, balance.ColorIOTA) + } + } else { + consumedFunds[color] -= amount + + if consumedFunds[color] == 0 { + delete(consumedFunds, color) + } + } + } + } + + // build outputs for remainder + if len(consumedFunds) != 0 { + if _, addressExists := outputsByColor[sendFundsOptions.RemainderAddress.Address]; !addressExists { + outputsByColor[sendFundsOptions.RemainderAddress.Address] = make(map[balance.Color]uint64) + } + + for color, amount := range consumedFunds { + outputsByColor[sendFundsOptions.RemainderAddress.Address][color] += amount + } + } + + // construct result + outputsBySlice := make(map[address.Address][]*balance.Balance) + for addr, outputs := range outputsByColor { + outputsBySlice[addr] = make([]*balance.Balance, 0) + for color, amount := range outputs { + outputsBySlice[addr] = append(outputsBySlice[addr], balance.New(color, int64(amount))) + } + } + outputs = transaction.NewOutputs(outputsBySlice) + + return +} diff --git a/client/wallet/wallet_test.go b/client/wallet/wallet_test.go new file mode 100644 index 0000000000000000000000000000000000000000..34947c3815be59c1f7d705910e26baa9d6c7a150 --- /dev/null +++ b/client/wallet/wallet_test.go @@ -0,0 +1,236 @@ +package wallet + +import ( + "crypto/rand" + "testing" + + walletaddr "github.com/iotaledger/goshimmer/client/wallet/packages/address" + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/hive.go/bitmask" + "github.com/stretchr/testify/assert" +) + +func TestWallet_SendFunds(t *testing.T) { + // create test seed + senderSeed := walletseed.NewSeed() + receiverSeed := walletseed.NewSeed() + + // define sub-tests by providing a list of parameters and a validator function + testCases := []struct { + name string + parameters []SendFundsOption + validator func(t *testing.T, tx *transaction.Transaction, err error) + }{ + // test if not providing a destination triggers an error + { + name: "missingDestination", + parameters: []SendFundsOption{ + Remainder(walletaddr.AddressEmpty), + }, + validator: func(t *testing.T, tx *transaction.Transaction, err error) { + assert.True(t, tx == nil, "the transaction should be nil") + assert.Error(t, err, "calling SendFunds without a Destination should trigger an error") + assert.Equal(t, "you need to provide at least one Destination for a valid transfer to be issued", err.Error(), "the error message is wrong") + }, + }, + + // test if providing an invalid destination (amount <= 0) triggers an error + { + name: "zeroAmount", + parameters: []SendFundsOption{ + Destination(address.Empty, 1), + Destination(address.Empty, 0), + Destination(address.Empty, 123), + }, + validator: func(t *testing.T, tx *transaction.Transaction, err error) { + assert.True(t, tx == nil, "the transaction should be nil") + assert.Error(t, err, "calling SendFunds without an invalid Destination (amount <= 0) should trigger an error") + assert.Equal(t, "the amount provided in the destinations needs to be larger than 0", err.Error(), "the error message is wrong") + }, + }, + + // test if a valid transaction can be created + { + name: "validTransfer", + parameters: []SendFundsOption{ + Destination(receiverSeed.Address(0).Address, 1200), + }, + validator: func(t *testing.T, tx *transaction.Transaction, err error) { + assert.False(t, tx == nil, "there should be a transaction created") + assert.Nil(t, err) + }, + }, + + // test if a valid transaction having a colored coin can be created + { + name: "validColoredTransfer", + parameters: []SendFundsOption{ + Destination(receiverSeed.Address(0).Address, 1200, balance.ColorNew), + }, + validator: func(t *testing.T, tx *transaction.Transaction, err error) { + assert.False(t, tx == nil, "there should be a transaction created") + assert.Nil(t, err) + }, + }, + } + + // execute sub-tests and hand in the results to the validator function + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + // create mocked connector + mockedConnector := newMockConnector( + &Output{ + Address: senderSeed.Address(0).Address, + TransactionID: transaction.GenesisID, + Balances: map[balance.Color]uint64{ + balance.ColorIOTA: 1337, + {3}: 1338, + }, + InclusionState: InclusionState{ + Liked: true, + Confirmed: true, + }, + }, + &Output{ + Address: senderSeed.Address(0).Address, + TransactionID: transaction.ID{3}, + Balances: map[balance.Color]uint64{ + balance.ColorIOTA: 663, + {4}: 1338, + }, + InclusionState: InclusionState{ + Liked: true, + Confirmed: true, + }, + }, + ) + + // create our test wallet + wallet := New( + Import(senderSeed, 1, []bitmask.BitMask{}, NewAssetRegistry()), + GenericConnector(mockedConnector), + ) + + // validate the result of the function call + tx, err := wallet.SendFunds(testCase.parameters...) + testCase.validator(t, tx, err) + }) + } +} + +type mockConnector struct { + outputs map[address.Address]map[transaction.ID]*Output +} + +func (connector *mockConnector) RequestFaucetFunds(addr walletaddr.Address) (err error) { + // generate random transaction id + idBytes := make([]byte, transaction.IDLength) + _, err = rand.Read(idBytes) + if err != nil { + return + } + transactionID, _, err := transaction.IDFromBytes(idBytes) + if err != nil { + return + } + + newOutput := &Output{ + Address: addr.Address, + TransactionID: transactionID, + Balances: map[balance.Color]uint64{ + balance.ColorIOTA: 1337, + }, + InclusionState: InclusionState{ + Liked: true, + Confirmed: true, + Rejected: false, + Conflicting: false, + Spent: false, + }, + } + + if _, addressExists := connector.outputs[addr.Address]; !addressExists { + connector.outputs[addr.Address] = make(map[transaction.ID]*Output) + } + connector.outputs[addr.Address][transactionID] = newOutput + + return +} + +func (connector *mockConnector) SendTransaction(tx *transaction.Transaction) (err error) { + // mark outputs as spent + tx.Inputs().ForEach(func(outputId transaction.OutputID) bool { + connector.outputs[outputId.Address()][outputId.TransactionID()].InclusionState.Spent = true + + return true + }) + + // create new outputs + tx.Outputs().ForEach(func(addr address.Address, balances []*balance.Balance) bool { + // initialize missing address entry + if _, addressExists := connector.outputs[addr]; !addressExists { + connector.outputs[addr] = make(map[transaction.ID]*Output) + } + + // translate balances to mockConnector specific balances + outputBalances := make(map[balance.Color]uint64) + for _, coloredBalance := range balances { + outputBalances[coloredBalance.Color] += uint64(coloredBalance.Value) + } + + // store new output + connector.outputs[addr][tx.ID()] = &Output{ + Address: addr, + TransactionID: tx.ID(), + Balances: outputBalances, + InclusionState: InclusionState{ + Liked: true, + Confirmed: true, + Rejected: false, + Conflicting: false, + Spent: false, + }, + } + + return true + }) + + return +} + +func newMockConnector(outputs ...*Output) (connector *mockConnector) { + connector = &mockConnector{ + outputs: make(map[address.Address]map[transaction.ID]*Output), + } + + for _, output := range outputs { + if _, addressExists := connector.outputs[output.Address]; !addressExists { + connector.outputs[output.Address] = make(map[transaction.ID]*Output) + } + + connector.outputs[output.Address][output.TransactionID] = output + } + + return +} + +func (connector *mockConnector) UnspentOutputs(addresses ...walletaddr.Address) (outputs map[walletaddr.Address]map[transaction.ID]*Output, err error) { + outputs = make(map[walletaddr.Address]map[transaction.ID]*Output) + + for _, addr := range addresses { + for transactionID, output := range connector.outputs[addr.Address] { + if !output.InclusionState.Spent { + if _, outputsExist := outputs[addr]; !outputsExist { + outputs[addr] = make(map[transaction.ID]*Output) + } + + outputs[addr][transactionID] = output + } + } + } + + return +} diff --git a/client/wallet/webconnector.go b/client/wallet/webconnector.go new file mode 100644 index 0000000000000000000000000000000000000000..97004737cb7b26d6e40fd94a4c86ccbc93906924 --- /dev/null +++ b/client/wallet/webconnector.go @@ -0,0 +1,132 @@ +package wallet + +import ( + "net/http" + + "github.com/iotaledger/goshimmer/client" + walletaddr "github.com/iotaledger/goshimmer/client/wallet/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" +) + +// WebConnector implements a connector that uses the web API to connect to a node to implement the required functions +// for the wallet. +type WebConnector struct { + client *client.GoShimmerAPI +} + +// NewWebConnector is the constructor for the WebConnector. +func NewWebConnector(baseURL string, httpClient ...http.Client) *WebConnector { + return &WebConnector{ + client: client.NewGoShimmerAPI(baseURL, httpClient...), + } +} + +// ServerStatus retrieves the connected server status with Info api. +func (webConnector *WebConnector) ServerStatus() (status ServerStatus, err error) { + response, err := webConnector.client.Info() + if err != nil { + return + } + + status.ID = response.IdentityID + status.Synced = response.Synced + status.Version = response.Version + + return +} + +// RequestFaucetFunds request some funds from the faucet for test purposes. +func (webConnector *WebConnector) RequestFaucetFunds(addr walletaddr.Address) (err error) { + _, err = webConnector.client.SendFaucetRequest(addr.String()) + + return +} + +// UnspentOutputs returns the outputs of transactions on the given addresses that have not been spent yet. +func (webConnector WebConnector) UnspentOutputs(addresses ...walletaddr.Address) (unspentOutputs map[walletaddr.Address]map[transaction.ID]*Output, err error) { + // build reverse lookup table + arguments for client call + addressReverseLookupTable := make(map[string]walletaddr.Address) + base58EncodedAddresses := make([]string, len(addresses)) + for i, addr := range addresses { + base58EncodedAddresses[i] = addr.String() + addressReverseLookupTable[addr.String()] = addr + } + + // request unspent outputs + response, err := webConnector.client.GetUnspentOutputs(base58EncodedAddresses) + if err != nil { + return + } + + // build result + unspentOutputs = make(map[walletaddr.Address]map[transaction.ID]*Output) + for _, unspentOutput := range response.UnspentOutputs { + // lookup wallet address from raw address + addr, addressRequested := addressReverseLookupTable[unspentOutput.Address] + if !addressRequested { + panic("the server returned an unrequested address") + } + + // iterate through outputs + for _, output := range unspentOutput.OutputIDs { + // parse output id + outputID, parseErr := transaction.OutputIDFromBase58(output.ID) + if parseErr != nil { + err = parseErr + + return + } + + // build balances map + balancesByColor := make(map[balance.Color]uint64) + for _, bal := range output.Balances { + color := colorFromString(bal.Color) + balancesByColor[color] += uint64(bal.Value) + } + + // build output + walletOutput := &Output{ + Address: addr.Address, + TransactionID: outputID.TransactionID(), + Balances: balancesByColor, + InclusionState: InclusionState{ + Liked: output.InclusionState.Liked, + Confirmed: output.InclusionState.Confirmed, + Rejected: output.InclusionState.Rejected, + Conflicting: output.InclusionState.Conflicting, + Spent: false, + }, + } + + // store output in result + if _, addressExists := unspentOutputs[addr]; !addressExists { + unspentOutputs[addr] = make(map[transaction.ID]*Output) + } + unspentOutputs[addr][walletOutput.TransactionID] = walletOutput + } + } + + return +} + +// SendTransaction sends a new transaction to the network. +func (webConnector WebConnector) SendTransaction(tx *transaction.Transaction) (err error) { + _, err = webConnector.client.SendTransaction(tx.Bytes()) + + return +} + +// colorFromString is an internal utility method that parses the given string into a Color. +func colorFromString(colorStr string) (color balance.Color) { + if colorStr == "IOTA" { + color = balance.ColorIOTA + } else { + t, _ := transaction.IDFromBase58(colorStr) + color, _, _ = balance.ColorFromBytes(t.Bytes()) + } + return +} + +// Interface contract: make compiler warn if the interface is not implemented correctly. +var _ Connector = &WebConnector{} diff --git a/dapps/faucet/packages/faucet.go b/dapps/faucet/packages/faucet.go index fbbcc66c034d5d6c556cd978dd7c1fb63c2b8556..b8f40d4f7af64a6f1797fe5e279714640499bbd4 100644 --- a/dapps/faucet/packages/faucet.go +++ b/dapps/faucet/packages/faucet.go @@ -6,6 +6,7 @@ import ( "sync" "time" + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" faucetpayload "github.com/iotaledger/goshimmer/dapps/faucet/packages/payload" "github.com/iotaledger/goshimmer/dapps/valuetransfers" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" @@ -13,7 +14,6 @@ import ( "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" "github.com/iotaledger/goshimmer/packages/binary/datastructure/orderedmap" "github.com/iotaledger/goshimmer/packages/binary/messagelayer/message" "github.com/iotaledger/goshimmer/plugins/issuer" @@ -31,7 +31,7 @@ var ( func New(seed []byte, tokensPerRequest int64, blacklistCapacity int, maxTxBookedAwaitTime time.Duration) *Faucet { return &Faucet{ tokensPerRequest: tokensPerRequest, - wallet: wallet.New(seed), + seed: walletseed.NewSeed(seed), maxTxBookedAwaitTime: maxTxBookedAwaitTime, blacklist: orderedmap.New(), blacklistCapacity: blacklistCapacity, @@ -43,8 +43,8 @@ type Faucet struct { sync.Mutex // the amount of tokens to send to every request tokensPerRequest int64 - // the wallet instance of the faucet holding the tokens - wallet *wallet.Wallet + // the seed instance of the faucet holding the tokens + seed *walletseed.Seed // the time to await for the transaction fulfilling a funding request // to become booked in the value layer maxTxBookedAwaitTime time.Duration @@ -106,7 +106,7 @@ func (f *Faucet) SendFunds(msg *message.Message) (m *message.Message, txID strin } for index := range addrsIndices { - tx.Sign(signaturescheme.ED25519(*f.wallet.Seed().KeyPair(index))) + tx.Sign(signaturescheme.ED25519(*f.seed.KeyPair(index))) } // prepare value payload with value factory @@ -142,7 +142,7 @@ func (f *Faucet) collectUTXOsForFunding() (outputIds []transaction.OutputID, add // get a list of address for inputs for i = 0; total > 0; i++ { - addr := f.wallet.Seed().Address(i) + addr := f.seed.Address(i).Address valuetransfers.Tangle().OutputsOnAddress(addr).Consume(func(output *tangle.Output) { if output.ConsumerCount() > 0 || total == 0 { return @@ -172,7 +172,7 @@ func (f *Faucet) collectUTXOsForFunding() (outputIds []transaction.OutputID, add func (f *Faucet) nextUnusedAddress() address.Address { var index uint64 for index = 0; ; index++ { - addr := f.wallet.Seed().Address(index) + addr := f.seed.Address(index).Address cachedOutputs := valuetransfers.Tangle().OutputsOnAddress(addr) if len(cachedOutputs) == 0 { // unused address diff --git a/dapps/valuetransfers/packages/address/address.go b/dapps/valuetransfers/packages/address/address.go index 16068a88d3e00c40cfe4eacfba4c98c851c28a36..ded68269b6027ac6ac8ec3d3e15738f6d3744371 100644 --- a/dapps/valuetransfers/packages/address/address.go +++ b/dapps/valuetransfers/packages/address/address.go @@ -133,5 +133,8 @@ func (address Address) String() string { return base58.Encode(address.Bytes()) } +// Empty represents the 0-value of an address and therefore represents the "empty" address value +var Empty = Address{} + // Length contains the length of an address (digest length = 32 + version byte length = 1). const Length = 33 diff --git a/dapps/valuetransfers/packages/tangle/signature_filter_test.go b/dapps/valuetransfers/packages/tangle/signature_filter_test.go index 7f511ac36e2389c3adbae194f11bac2dc901ca36..2ac1a596ed3bcc96dd5cd1afaacdf426fa7f705f 100644 --- a/dapps/valuetransfers/packages/tangle/signature_filter_test.go +++ b/dapps/valuetransfers/packages/tangle/signature_filter_test.go @@ -9,7 +9,6 @@ import ( "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" valuePayload "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" "github.com/iotaledger/goshimmer/packages/binary/messagelayer/message" "github.com/iotaledger/goshimmer/packages/binary/messagelayer/messagefactory" "github.com/iotaledger/goshimmer/packages/binary/messagelayer/messageparser" @@ -28,7 +27,7 @@ func TestSignatureFilter(t *testing.T) { messageParser := newSyncMessageParser(NewSignatureFilter()) // create helper instances - seed := wallet.NewSeed() + seed := newSeed() messageFactory := messagefactory.New(mapdb.NewMapDB(), []byte("sequenceKey"), identity.GenerateLocalIdentity(), tipselector.New()) // 1. test value message without signatures diff --git a/dapps/valuetransfers/packages/tangle/snapshot_test.go b/dapps/valuetransfers/packages/tangle/snapshot_test.go index 7ef8b153864d90fdfebd5034ec22ab676f87f6bb..f1b4696471666e88ae4a3649a558ef6f80ec0f15 100644 --- a/dapps/valuetransfers/packages/tangle/snapshot_test.go +++ b/dapps/valuetransfers/packages/tangle/snapshot_test.go @@ -8,7 +8,6 @@ import ( "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" "github.com/iotaledger/hive.go/kvstore/mapdb" "github.com/stretchr/testify/assert" ) @@ -44,7 +43,7 @@ func TestLoadSnapshot(t *testing.T) { func TestSnapshotMarshalUnmarshal(t *testing.T) { const genesisBalance = 1000000000 - seed := wallet.NewSeed() + seed := newSeed() genesisAddr := seed.Address(GENESIS) snapshot := Snapshot{ diff --git a/dapps/valuetransfers/packages/tangle/tangle_scenario_test.go b/dapps/valuetransfers/packages/tangle/tangle_scenario_test.go index 5d185f9637fef1a7bef59f8e8b69138936f2ac37..542690dd8994a847f383ec96a9be6259165093e7 100644 --- a/dapps/valuetransfers/packages/tangle/tangle_scenario_test.go +++ b/dapps/valuetransfers/packages/tangle/tangle_scenario_test.go @@ -9,7 +9,6 @@ import ( "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" "github.com/iotaledger/hive.go/kvstore/mapdb" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -34,12 +33,12 @@ const ( // TODO: clean up create scenario with some helper functions: DRY! // preparePropagationScenario1 creates a tangle according to `img/scenario1.png`. -func preparePropagationScenario1(t *testing.T) (*eventTangle, map[string]*transaction.Transaction, map[string]*payload.Payload, map[string]branchmanager.BranchID, *wallet.Seed) { +func preparePropagationScenario1(t *testing.T) (*eventTangle, map[string]*transaction.Transaction, map[string]*payload.Payload, map[string]branchmanager.BranchID, *seed) { // create tangle tangle := newEventTangle(t, New(mapdb.NewMapDB())) // create seed for testing - seed := wallet.NewSeed() + seed := newSeed() // initialize tangle with genesis block (+GENESIS) tangle.LoadSnapshot(map[transaction.ID]map[address.Address][]*balance.Balance{ @@ -542,7 +541,7 @@ func preparePropagationScenario1(t *testing.T) (*eventTangle, map[string]*transa } // preparePropagationScenario1 creates a tangle according to `img/scenario2.png`. -func preparePropagationScenario2(t *testing.T) (*eventTangle, map[string]*transaction.Transaction, map[string]*payload.Payload, map[string]branchmanager.BranchID, *wallet.Seed) { +func preparePropagationScenario2(t *testing.T) (*eventTangle, map[string]*transaction.Transaction, map[string]*payload.Payload, map[string]branchmanager.BranchID, *seed) { tangle, transactions, valueObjects, branches, seed := preparePropagationScenario1(t) // [-C, H+] diff --git a/dapps/valuetransfers/packages/tangle/test_util.go b/dapps/valuetransfers/packages/tangle/test_util.go new file mode 100644 index 0000000000000000000000000000000000000000..dfb730318f273ae9c1e4d65812b30bd78c2af5f2 --- /dev/null +++ b/dapps/valuetransfers/packages/tangle/test_util.go @@ -0,0 +1,20 @@ +package tangle + +import ( + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/hive.go/crypto/ed25519" +) + +type seed struct { + *ed25519.Seed +} + +func newSeed(optionalSeedBytes ...[]byte) *seed { + return &seed{ + ed25519.NewSeed(optionalSeedBytes...), + } +} + +func (seed *seed) Address(index uint64) address.Address { + return address.FromED25519PubKey(seed.Seed.KeyPair(index).PublicKey) +} diff --git a/dapps/valuetransfers/packages/test/tangle_test.go b/dapps/valuetransfers/packages/test/tangle_test.go index a45803700ff263a6b7d8e293f386c17f04698071..82be2f4c6ef761c993b6f395e0dd741174333b7c 100644 --- a/dapps/valuetransfers/packages/test/tangle_test.go +++ b/dapps/valuetransfers/packages/test/tangle_test.go @@ -10,13 +10,13 @@ import ( "github.com/stretchr/testify/assert" + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/consensus" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" ) func TestTangle_ValueTransfer(t *testing.T) { @@ -28,31 +28,31 @@ func TestTangle_ValueTransfer(t *testing.T) { ledgerState := tangle.NewLedgerState(valueTangle) // initialize seed - seed := wallet.NewSeed() + seed := walletseed.NewSeed() // setup consensus rules consensus.NewFCOB(valueTangle, 0) // check if ledger empty first - assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(seed.Address(0))) - assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(seed.Address(1))) + assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(seed.Address(0).Address)) + assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(seed.Address(1).Address)) // load snapshot valueTangle.LoadSnapshot(map[transaction.ID]map[address.Address][]*balance.Balance{ transaction.GenesisID: { - seed.Address(0): []*balance.Balance{ + seed.Address(0).Address: []*balance.Balance{ balance.New(balance.ColorIOTA, 337), }, - seed.Address(1): []*balance.Balance{ + seed.Address(1).Address: []*balance.Balance{ balance.New(balance.ColorIOTA, 1000), }, }, }) // check if balance exists after loading snapshot - assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 337}, ledgerState.Balances(seed.Address(0))) - assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 1000}, ledgerState.Balances(seed.Address(1))) + assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 337}, ledgerState.Balances(seed.Address(0).Address)) + assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 1000}, ledgerState.Balances(seed.Address(1).Address)) // introduce logic to record liked payloads recordedLikedPayloads, resetRecordedLikedPayloads := recordLikedPayloads(valueTangle) @@ -61,8 +61,8 @@ func TestTangle_ValueTransfer(t *testing.T) { outputAddress1 := address.Random() attachedPayload1 := payload.New(payload.GenesisID, payload.GenesisID, transaction.New( transaction.NewInputs( - transaction.NewOutputID(seed.Address(0), transaction.GenesisID), - transaction.NewOutputID(seed.Address(1), transaction.GenesisID), + transaction.NewOutputID(seed.Address(0).Address, transaction.GenesisID), + transaction.NewOutputID(seed.Address(1).Address, transaction.GenesisID), ), transaction.NewOutputs(map[address.Address][]*balance.Balance{ @@ -78,8 +78,8 @@ func TestTangle_ValueTransfer(t *testing.T) { }) // check if old addresses are empty and new addresses are filled - assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(seed.Address(0))) - assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(seed.Address(1))) + assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(seed.Address(0).Address)) + assert.Equal(t, map[balance.Color]int64{}, ledgerState.Balances(seed.Address(1).Address)) assert.Equal(t, map[balance.Color]int64{balance.ColorIOTA: 1337}, ledgerState.Balances(outputAddress1)) assert.Equal(t, 1, len(recordedLikedPayloads)) assert.Contains(t, recordedLikedPayloads, attachedPayload1.ID()) @@ -90,8 +90,8 @@ func TestTangle_ValueTransfer(t *testing.T) { outputAddress2 := address.Random() valueTangle.AttachPayloadSync(payload.New(payload.GenesisID, payload.GenesisID, transaction.New( transaction.NewInputs( - transaction.NewOutputID(seed.Address(0), transaction.GenesisID), - transaction.NewOutputID(seed.Address(1), transaction.GenesisID), + transaction.NewOutputID(seed.Address(0).Address, transaction.GenesisID), + transaction.NewOutputID(seed.Address(1).Address, transaction.GenesisID), ), transaction.NewOutputs(map[address.Address][]*balance.Balance{ diff --git a/dapps/valuetransfers/packages/wallet/wallet.go b/dapps/valuetransfers/packages/wallet/wallet.go deleted file mode 100644 index 88ab072fa649fa6840db5fc2df8c525dc59fa9a3..0000000000000000000000000000000000000000 --- a/dapps/valuetransfers/packages/wallet/wallet.go +++ /dev/null @@ -1,20 +0,0 @@ -package wallet - -// Wallet represents a simple cryptocurrency wallet for the IOTA tangle. It contains the logic to manage the movement of -// funds. -type Wallet struct { - seed *Seed -} - -// New is the factory method of the wallet. It either creates a new wallet or restores the wallet backup that is handed -// in as an optional parameter. -func New(optionalRecoveryBytes ...[]byte) *Wallet { - return &Wallet{ - seed: NewSeed(optionalRecoveryBytes...), - } -} - -// Seed returns the seed of this wallet that is used to generate all of the wallets addresses and private keys. -func (wallet *Wallet) Seed() *Seed { - return wallet.seed -} diff --git a/tools/cli-wallet/address.go b/tools/cli-wallet/address.go new file mode 100644 index 0000000000000000000000000000000000000000..2eb7c4b68bd642996df2ea2462b3f16af49c0a3a --- /dev/null +++ b/tools/cli-wallet/address.go @@ -0,0 +1,134 @@ +package main + +import ( + "flag" + "fmt" + "os" + "text/tabwriter" + + "github.com/iotaledger/goshimmer/client/wallet" +) + +func execAddressCommand(command *flag.FlagSet, cliWallet *wallet.Wallet) { + command.Usage = func() { + printUsage(command) + } + + receivePtr := command.Bool("receive", false, "show the latest receive address") + newReceiveAddressPtr := command.Bool("new", false, "generate a new receive address") + listPtr := command.Bool("list", false, "list all addresses") + listUnspentPtr := command.Bool("listunspent", false, "list all unspent addresses") + listSpentPtr := command.Bool("listspent", false, "list all spent addresses") + helpPtr := command.Bool("help", false, "display this help screen") + + err := command.Parse(os.Args[2:]) + if err != nil { + printUsage(command, err.Error()) + } + if *helpPtr { + printUsage(command) + } + + // sanitize flags + setFlagCount := 0 + if *receivePtr { + setFlagCount++ + } + if *listPtr { + setFlagCount++ + } + if *listUnspentPtr { + setFlagCount++ + } + if *listSpentPtr { + setFlagCount++ + } + if *newReceiveAddressPtr { + setFlagCount++ + } + if setFlagCount == 0 { + printUsage(command) + } + if setFlagCount > 1 { + printUsage(command, "please provide only one option at a time") + } + + if *receivePtr { + fmt.Println() + fmt.Println("Latest Receive Address: " + cliWallet.ReceiveAddress().String()) + } + + if *newReceiveAddressPtr { + fmt.Println() + fmt.Println("New Receive Address: " + cliWallet.NewReceiveAddress().String()) + } + + if *listPtr { + // initialize tab writer + w := new(tabwriter.Writer) + w.Init(os.Stdout, 0, 8, 2, '\t', 0) + defer w.Flush() + + // print header + fmt.Println() + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", "INDEX", "ADDRESS", "SPENT") + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", "-----", "--------------------------------------------", "-----") + + addressPrinted := false + for _, addr := range cliWallet.AddressManager().Addresses() { + _, _ = fmt.Fprintf(w, "%d\t%s\t%t\n", addr.Index, addr.String(), cliWallet.AddressManager().IsAddressSpent(addr.Index)) + + addressPrinted = true + } + + if !addressPrinted { + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", "<EMPTY>", "<EMPTY>", "<EMPTY>") + } + } + + if *listUnspentPtr { + // initialize tab writer + w := new(tabwriter.Writer) + w.Init(os.Stdout, 0, 8, 2, '\t', 0) + defer w.Flush() + + // print header + fmt.Println() + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", "INDEX", "ADDRESS", "SPENT") + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", "-------", "--------------------------------------------", "-------") + + addressPrinted := false + for _, addr := range cliWallet.AddressManager().UnspentAddresses() { + _, _ = fmt.Fprintf(w, "%d\t%s\t%t\n", addr.Index, addr.String(), cliWallet.AddressManager().IsAddressSpent(addr.Index)) + + addressPrinted = true + } + + if !addressPrinted { + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", "<EMPTY>", "<EMPTY>", "<EMPTY>") + } + } + + if *listSpentPtr { + // initialize tab writer + w := new(tabwriter.Writer) + w.Init(os.Stdout, 0, 8, 2, '\t', 0) + defer w.Flush() + + // print header + fmt.Println() + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", "INDEX", "ADDRESS", "SPENT") + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", "-------", "--------------------------------------------", "-------") + + addressPrinted := false + for _, addr := range cliWallet.AddressManager().SpentAddresses() { + _, _ = fmt.Fprintf(w, "%d\t%s\t%t\n", addr.Index, addr.String(), cliWallet.AddressManager().IsAddressSpent(addr.Index)) + + addressPrinted = true + } + + if !addressPrinted { + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", "<EMPTY>", "<EMPTY>", "<EMPTY>") + } + } +} diff --git a/tools/cli-wallet/balance.go b/tools/cli-wallet/balance.go new file mode 100644 index 0000000000000000000000000000000000000000..f4244505b6238efc5ae37adcee706ec67cc0d627 --- /dev/null +++ b/tools/cli-wallet/balance.go @@ -0,0 +1,47 @@ +package main + +import ( + "flag" + "fmt" + "os" + "text/tabwriter" + + "github.com/iotaledger/goshimmer/client/wallet" +) + +func execBalanceCommand(command *flag.FlagSet, cliWallet *wallet.Wallet) { + err := command.Parse(os.Args[2:]) + if err != nil { + panic(err) + } + + confirmedBalance, pendingBalance, err := cliWallet.Balance() + if err != nil { + printUsage(nil, err.Error()) + } + + // initialize tab writer + w := new(tabwriter.Writer) + w.Init(os.Stdout, 0, 8, 2, '\t', 0) + defer w.Flush() + + // print header + fmt.Println() + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "STATUS", "BALANCE", "COLOR", "TOKEN NAME") + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "------", "---------------", "--------------------------------------------", "-------------------------") + + // print empty if no balances founds + if len(confirmedBalance) == 0 && len(pendingBalance) == 0 { + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "<EMPTY>", "<EMPTY>", "<EMPTY>", "<EMPTY>") + + return + } + + // print balances + for color, amount := range confirmedBalance { + _, _ = fmt.Fprintf(w, "%s\t%d %s\t%s\t%s\n", "[ OK ]", amount, cliWallet.AssetRegistry().Symbol(color), color.String(), cliWallet.AssetRegistry().Name(color)) + } + for color, amount := range pendingBalance { + _, _ = fmt.Fprintf(w, "%s\t%d\t%s\t%s\n", "[PEND]", amount, color.String(), cliWallet.AssetRegistry().Name(color)) + } +} diff --git a/tools/cli-wallet/build.sh b/tools/cli-wallet/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..09d5182a757bf41b42be8a56e6feffdcd3325546 --- /dev/null +++ b/tools/cli-wallet/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +echo "Building executables..." + +GOOS=windows GOARCH=amd64 go build -o cli-wallet_Windows_x86_64.exe +echo "Windows version created" +GOOS=linux GOARCH=amd64 go build -o cli-wallet_Linux_x86_64 +echo "Linux version created" +GOOS=darwin GOARCH=amd64 go build -o cli-wallet_macOS_x86_64 +echo "MAC OSX version created" + +echo "All done!" \ No newline at end of file diff --git a/tools/cli-wallet/config.go b/tools/cli-wallet/config.go new file mode 100644 index 0000000000000000000000000000000000000000..781f19bb95bf42e95d91c20e0aa26db5a9b784cc --- /dev/null +++ b/tools/cli-wallet/config.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "os" +) + +// config type that defines the config structure +type configuration struct { + WebAPI string +} + +// internal variable that holds the config +var config = configuration{} + +// load the config file +func loadConfig() { + // open config file + file, err := os.Open("config.json") + if err != nil { + if !os.IsNotExist(err) { + panic(err) + } + + if err = ioutil.WriteFile("config.json", []byte("{\n \"WebAPI\": \"http://127.0.0.1:8080\"\n}"), 0644); err != nil { + panic(err) + } + if file, err = os.Open("config.json"); err != nil { + panic(err) + } + } + defer file.Close() + + // decode config file + if err = json.NewDecoder(file).Decode(&config); err != nil { + panic(err) + } +} diff --git a/tools/cli-wallet/create_asset.go b/tools/cli-wallet/create_asset.go new file mode 100644 index 0000000000000000000000000000000000000000..ba187b38aaebc73dff435004c16c8adc4761c483 --- /dev/null +++ b/tools/cli-wallet/create_asset.go @@ -0,0 +1,49 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strconv" + + "github.com/iotaledger/goshimmer/client/wallet" +) + +func execCreateAssetCommand(command *flag.FlagSet, cliWallet *wallet.Wallet) { + command.Usage = func() { + printUsage(command) + } + + helpPtr := command.Bool("help", false, "show this help screen") + amountPtr := command.Uint64("amount", 0, "the amount of tokens to be created") + namePtr := command.String("name", "", "the name of the tokens to create") + symbolPtr := command.String("symbol", "", "the symbol of the tokens to create") + + err := command.Parse(os.Args[2:]) + if err != nil { + printUsage(command, err.Error()) + } + if *helpPtr { + printUsage(command) + } + + if *amountPtr == 0 { + printUsage(command) + } + + if *namePtr == "" { + printUsage(command, "you need to provide a name for you asset") + } + + assetColor, err := cliWallet.CreateAsset(wallet.Asset{ + Name: *namePtr, + Symbol: *symbolPtr, + Amount: *amountPtr, + }) + if err != nil { + printUsage(command, err.Error()) + } + + fmt.Println() + fmt.Println("Creating " + strconv.Itoa(int(*amountPtr)) + " tokens with the color '" + assetColor.String() + "' ... [DONE]") +} diff --git a/tools/cli-wallet/lib.go b/tools/cli-wallet/lib.go new file mode 100644 index 0000000000000000000000000000000000000000..280960954ca593f10d6d25e3bca9cb669737dce7 --- /dev/null +++ b/tools/cli-wallet/lib.go @@ -0,0 +1,166 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "unsafe" + + "github.com/iotaledger/goshimmer/client/wallet" + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" + "github.com/iotaledger/hive.go/bitmask" + "github.com/iotaledger/hive.go/crypto/ed25519" + "github.com/iotaledger/hive.go/marshalutil" + "github.com/mr-tron/base58" +) + +func printBanner() { + fmt.Println("IOTA Pollen CLI-Wallet 0.1") +} + +func loadWallet() *wallet.Wallet { + seed, lastAddressIndex, spentAddresses, assetRegistry, err := importWalletStateFile("wallet.dat") + if err != nil { + panic(err) + } + + return wallet.New( + wallet.WebAPI(config.WebAPI), + wallet.Import(seed, lastAddressIndex, spentAddresses, assetRegistry), + ) +} + +func importWalletStateFile(filename string) (seed *walletseed.Seed, lastAddressIndex uint64, spentAddresses []bitmask.BitMask, assetRegistry *wallet.AssetRegistry, err error) { + walletStateBytes, err := ioutil.ReadFile(filename) + if err != nil { + if !os.IsNotExist(err) { + return + } + + if len(os.Args) < 2 || os.Args[1] != "init" { + printUsage(nil, "no wallet file (wallet.dat) found: please call \""+filepath.Base(os.Args[0])+" init\"") + } + + seed = walletseed.NewSeed() + lastAddressIndex = 0 + spentAddresses = []bitmask.BitMask{} + err = nil + + fmt.Println("GENERATING NEW WALLET ... [DONE]") + fmt.Println() + fmt.Println("================================================================") + fmt.Println("!!! PLEASE CREATE A BACKUP OF YOUR SEED !!!") + fmt.Println("!!! !!!") + fmt.Println("!!! " + base58.Encode(seed.Bytes()) + " !!!") + fmt.Println("!!! !!!") + fmt.Println("!!! PLEASE CREATE A BACKUP OF YOUR SEED !!!") + fmt.Println("================================================================") + + return + } + + if len(os.Args) >= 2 && os.Args[1] == "init" { + printUsage(nil, "please remove the wallet.dat before trying to create a new wallet") + } + + marshalUtil := marshalutil.New(walletStateBytes) + + seedBytes, err := marshalUtil.ReadBytes(ed25519.SeedSize) + seed = walletseed.NewSeed(seedBytes) + if err != nil { + return + } + + lastAddressIndex, err = marshalUtil.ReadUint64() + if err != nil { + return + } + + assetRegistry, _, err = wallet.ParseAssetRegistry(marshalUtil) + + spentAddressesBytes := marshalUtil.ReadRemainingBytes() + spentAddresses = *(*[]bitmask.BitMask)(unsafe.Pointer(&spentAddressesBytes)) + + return +} + +func writeWalletStateFile(wallet *wallet.Wallet, filename string) { + var skipRename bool + info, err := os.Stat(filename) + if err != nil { + if !os.IsNotExist(err) { + panic(err) + } + + skipRename = true + } + if err == nil && info.IsDir() { + panic("found directory instead of file at " + filename) + } + + if !skipRename { + err = os.Rename(filename, filename+".bkp") + if err != nil && os.IsNotExist(err) { + panic(err) + } + } + + err = ioutil.WriteFile(filename, wallet.ExportState(), 0644) + if err != nil { + panic(err) + } +} + +func printUsage(command *flag.FlagSet, optionalErrorMessage ...string) { + if len(optionalErrorMessage) >= 1 { + _, _ = fmt.Fprintf(os.Stderr, "\n") + _, _ = fmt.Fprintf(os.Stderr, "ERROR:\n "+optionalErrorMessage[0]+"\n") + } + + if command == nil { + fmt.Println() + fmt.Println("USAGE:") + fmt.Println(" " + filepath.Base(os.Args[0]) + " [COMMAND]") + fmt.Println() + fmt.Println("COMMANDS:") + fmt.Println(" balance") + fmt.Println(" show the balances held by this wallet") + fmt.Println(" send-funds") + fmt.Println(" initiate a value transfer") + fmt.Println(" create-asset") + fmt.Println(" create an asset in the form of colored coins") + fmt.Println(" address") + fmt.Println(" start the address manager of this wallet") + fmt.Println(" request-funds") + fmt.Println(" request funds from the testnet-faucet") + fmt.Println(" init") + fmt.Println(" generate a new wallet using a random seed") + fmt.Println(" server-status") + fmt.Println(" display the server status") + fmt.Println(" help") + fmt.Println(" display this help screen") + + flag.PrintDefaults() + + if len(optionalErrorMessage) >= 1 { + os.Exit(1) + } + + os.Exit(0) + } + + fmt.Println() + fmt.Println("USAGE:") + fmt.Println(" " + filepath.Base(os.Args[0]) + " " + command.Name() + " [OPTIONS]") + fmt.Println() + fmt.Println("OPTIONS:") + command.PrintDefaults() + + if len(optionalErrorMessage) >= 1 { + os.Exit(1) + } + + os.Exit(0) +} diff --git a/tools/cli-wallet/main.go b/tools/cli-wallet/main.go new file mode 100644 index 0000000000000000000000000000000000000000..b69e44465fc716f090d1bfc9c53ae30eaac6d200 --- /dev/null +++ b/tools/cli-wallet/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "flag" + "fmt" + "os" +) + +// entry point for the program +func main() { + defer func() { + if r := recover(); r != nil { + _, _ = fmt.Fprintf(os.Stderr, "\nFATAL ERROR: "+r.(error).Error()) + os.Exit(1) + } + }() + + // print banner + initialize framework + printBanner() + loadConfig() + + // override Usage to use our custom method + flag.Usage = func() { + printUsage(nil) + } + + // load wallet + wallet := loadWallet() + defer writeWalletStateFile(wallet, "wallet.dat") + + // check if parameters potentially include sub commands + if len(os.Args) < 2 { + printUsage(nil) + } + + // define sub commands + balanceCommand := flag.NewFlagSet("balance", flag.ExitOnError) + sendFundsCommand := flag.NewFlagSet("send-funds", flag.ExitOnError) + createAssetCommand := flag.NewFlagSet("create-asset", flag.ExitOnError) + addressCommand := flag.NewFlagSet("address", flag.ExitOnError) + requestFaucetFundsCommand := flag.NewFlagSet("request-funds", flag.ExitOnError) + serverStatusCommand := flag.NewFlagSet("server-status", flag.ExitOnError) + + // switch logic according to provided sub command + switch os.Args[1] { + case "balance": + execBalanceCommand(balanceCommand, wallet) + case "address": + execAddressCommand(addressCommand, wallet) + case "send-funds": + execSendFundsCommand(sendFundsCommand, wallet) + case "create-asset": + execCreateAssetCommand(createAssetCommand, wallet) + case "request-funds": + execRequestFundsCommand(requestFaucetFundsCommand, wallet) + case "init": + fmt.Println() + fmt.Println("CREATING WALLET STATE FILE (wallet.dat) ... [DONE]") + case "server-status": + execServerStatusCommand(serverStatusCommand, wallet) + case "help": + printUsage(nil) + default: + printUsage(nil, "unknown [COMMAND]: "+os.Args[1]) + } +} diff --git a/tools/cli-wallet/requestfunds.go b/tools/cli-wallet/requestfunds.go new file mode 100644 index 0000000000000000000000000000000000000000..c392708e412bb3139f1017e8fa26d257f065d633 --- /dev/null +++ b/tools/cli-wallet/requestfunds.go @@ -0,0 +1,26 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/iotaledger/goshimmer/client/wallet" +) + +func execRequestFundsCommand(command *flag.FlagSet, cliWallet *wallet.Wallet) { + err := command.Parse(os.Args[2:]) + if err != nil { + printUsage(nil, err.Error()) + } + + fmt.Println() + fmt.Println("Requesting funds from faucet ... [PERFORMING POW] (this can take a while)") + + // request funds + err = cliWallet.RequestFaucetFunds() + if err != nil { + panic(err) + } + fmt.Println("Requesting funds from faucet ... [DONE]") +} diff --git a/tools/cli-wallet/sendfunds.go b/tools/cli-wallet/sendfunds.go new file mode 100644 index 0000000000000000000000000000000000000000..617f70e7d72222273761cb4153c5d64c3e2c796a --- /dev/null +++ b/tools/cli-wallet/sendfunds.go @@ -0,0 +1,71 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/iotaledger/goshimmer/client/wallet" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/mr-tron/base58" +) + +func execSendFundsCommand(command *flag.FlagSet, cliWallet *wallet.Wallet) { + helpPtr := command.Bool("help", false, "show this help screen") + addressPtr := command.String("dest-addr", "", "destination address for the transfer") + amountPtr := command.Int64("amount", 0, "the amount of tokens that are supposed to be sent") + colorPtr := command.String("color", "IOTA", "color of the tokens to transfer (optional)") + + err := command.Parse(os.Args[2:]) + if err != nil { + panic(err) + } + + if *helpPtr { + printUsage(command) + } + + if *addressPtr == "" { + printUsage(command, "dest-addr has to be set") + } + if *amountPtr <= 0 { + printUsage(command, "amount has to be set and be bigger than 0") + } + if *colorPtr == "" { + printUsage(command, "color must be set") + } + + destinationAddress, err := address.FromBase58(*addressPtr) + if err != nil { + printUsage(command, err.Error()) + } + + var color balance.Color + switch *colorPtr { + case "IOTA": + color = balance.ColorIOTA + case "NEW": + color = balance.ColorNew + default: + colorBytes, parseErr := base58.Decode(*colorPtr) + if parseErr != nil { + printUsage(command, parseErr.Error()) + } + + color, _, parseErr = balance.ColorFromBytes(colorBytes) + if parseErr != nil { + printUsage(command, parseErr.Error()) + } + } + + _, err = cliWallet.SendFunds( + wallet.Destination(destinationAddress, uint64(*amountPtr), color), + ) + if err != nil { + printUsage(command, err.Error()) + } + + fmt.Println() + fmt.Println("Sending funds ... [DONE]") +} diff --git a/tools/cli-wallet/serverstatus.go b/tools/cli-wallet/serverstatus.go new file mode 100644 index 0000000000000000000000000000000000000000..573ef0f88cd360482b0e745d37cb3f97066cfa8e --- /dev/null +++ b/tools/cli-wallet/serverstatus.go @@ -0,0 +1,27 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/iotaledger/goshimmer/client/wallet" +) + +func execServerStatusCommand(command *flag.FlagSet, cliWallet *wallet.Wallet) { + err := command.Parse(os.Args[2:]) + if err != nil { + printUsage(nil, err.Error()) + } + + fmt.Println() + + // request funds + status, err := cliWallet.ServerStatus() + if err != nil { + panic(err) + } + fmt.Println("Server ID: ", status.ID) + fmt.Println("Server Synced: ", status.Synced) + fmt.Println("Server Version: ", status.Version) +} diff --git a/tools/double-spend/double-spend.go b/tools/double-spend/double-spend.go index 65f0f56335df8904c79863c92d506c700f8f94a3..c41d95ad3d0f811fdc4e26dc1723a4f3be8c7ebf 100644 --- a/tools/double-spend/double-spend.go +++ b/tools/double-spend/double-spend.go @@ -7,12 +7,13 @@ import ( "time" "github.com/iotaledger/goshimmer/client" + "github.com/iotaledger/goshimmer/client/wallet" + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address/signaturescheme" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" valuepayload "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" ) func main() { @@ -79,7 +80,7 @@ func main() { // issue transactions which spend the same output conflictingTxs := make([]*transaction.Transaction, 2) conflictingMsgIDs := make([]string, 2) - receiverSeeds := make([]*wallet.Seed, 2) + receiverSeeds := make([]*walletseed.Seed, 2) var wg sync.WaitGroup for i := range conflictingTxs { @@ -90,13 +91,13 @@ func main() { fmt.Println(i) // create a new receiver wallet for the given conflict - receiverSeeds[i] = wallet.NewSeed() + receiverSeeds[i] = walletseed.NewSeed() destAddr := receiverSeeds[i].Address(0) tx := transaction.New( transaction.NewInputs(out), transaction.NewOutputs(map[address.Address][]*balance.Balance{ - destAddr: { + destAddr.Address: { {Value: 1337, Color: balance.ColorIOTA}, }, })) diff --git a/tools/genesis-snapshot/main.go b/tools/genesis-snapshot/main.go index 1a887e139ba8e606516899e781401e2700361de0..a53c1980bd77cd867e56bb607fe91977016bd5a1 100644 --- a/tools/genesis-snapshot/main.go +++ b/tools/genesis-snapshot/main.go @@ -4,10 +4,10 @@ import ( "log" "os" + "github.com/iotaledger/goshimmer/client/wallet" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" flag "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -33,7 +33,7 @@ func main() { log.Printf("creating snapshot %s...", snapshotFileName) genesisWallet := wallet.New() - genesisAddress := genesisWallet.Seed().Address(0) + genesisAddress := genesisWallet.Seed().Address(0).Address log.Println("genesis:") log.Printf("-> seed (base58): %s", genesisWallet.Seed().String()) diff --git a/tools/integration-tests/tester/framework/network.go b/tools/integration-tests/tester/framework/network.go index d72337f1553f376b4dae72247decb02e65ab2181..cc745a378847eb800c97a6a53f1c8c6a5abb52ea 100644 --- a/tools/integration-tests/tester/framework/network.go +++ b/tools/integration-tests/tester/framework/network.go @@ -9,7 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/client" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" "github.com/iotaledger/hive.go/crypto/ed25519" "github.com/iotaledger/hive.go/identity" ) @@ -103,11 +103,11 @@ func (n *Network) CreatePeer(c GoShimmerConfig) (*Peer, error) { config.SnapshotFilePath = snapshotFilePath // create wallet - var nodeWallet *wallet.Wallet + var nodeSeed *walletseed.Seed if c.Faucet == true { - nodeWallet = wallet.New(genesisSeed) + nodeSeed = walletseed.NewSeed(genesisSeed) } else { - nodeWallet = wallet.New() + nodeSeed = walletseed.NewSeed() } // create Docker container @@ -125,7 +125,7 @@ func (n *Network) CreatePeer(c GoShimmerConfig) (*Peer, error) { return nil, err } - peer, err := newPeer(name, identity.New(publicKey), container, nodeWallet, n) + peer, err := newPeer(name, identity.New(publicKey), container, nodeSeed, n) if err != nil { return nil, err } diff --git a/tools/integration-tests/tester/framework/peer.go b/tools/integration-tests/tester/framework/peer.go index 8da31c20ef4634f2dc408034b5046a9b7b589712..e3e540aa0d4d2c89a6db9f02eaac516d4d25fff6 100644 --- a/tools/integration-tests/tester/framework/peer.go +++ b/tools/integration-tests/tester/framework/peer.go @@ -6,7 +6,7 @@ import ( "time" "github.com/iotaledger/goshimmer/client" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" "github.com/iotaledger/goshimmer/plugins/webapi/autopeering" "github.com/iotaledger/hive.go/identity" ) @@ -25,8 +25,8 @@ type Peer struct { // the DockerContainer that this peer is running in *DockerContainer - // Wallet - *wallet.Wallet + // Seed + *walletseed.Seed chosen []autopeering.Neighbor accepted []autopeering.Neighbor @@ -34,7 +34,7 @@ type Peer struct { // newPeer creates a new instance of Peer with the given information. // dockerContainer needs to be started in order to determine the container's (and therefore peer's) IP correctly. -func newPeer(name string, identity *identity.Identity, dockerContainer *DockerContainer, wallet *wallet.Wallet, network *Network) (*Peer, error) { +func newPeer(name string, identity *identity.Identity, dockerContainer *DockerContainer, seed *walletseed.Seed, network *Network) (*Peer, error) { // after container is started we can get its IP ip, err := dockerContainer.IP(network.name) if err != nil { @@ -47,7 +47,7 @@ func newPeer(name string, identity *identity.Identity, dockerContainer *DockerCo Identity: identity, GoShimmerAPI: client.NewGoShimmerAPI(getWebAPIBaseURL(name), http.Client{Timeout: 30 * time.Second}), DockerContainer: dockerContainer, - Wallet: wallet, + Seed: seed, }, nil } diff --git a/tools/integration-tests/tester/tests/consensus/consensus_conflicts_test.go b/tools/integration-tests/tester/tests/consensus/consensus_conflicts_test.go index 4aec007c066a5feaeab1cb9fe530631228da5f4f..1e56f39a1ed79bf1a27c5f43a47aeadfec7c6006 100644 --- a/tools/integration-tests/tester/tests/consensus/consensus_conflicts_test.go +++ b/tools/integration-tests/tester/tests/consensus/consensus_conflicts_test.go @@ -7,11 +7,11 @@ import ( "github.com/iotaledger/goshimmer/tools/integration-tests/tester/framework" + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address/signaturescheme" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" "github.com/iotaledger/goshimmer/plugins/webapi/value/utils" "github.com/iotaledger/goshimmer/tools/integration-tests/tester/tests" "github.com/mr-tron/base58/base58" @@ -57,20 +57,20 @@ func TestConsensusFiftyFiftyOpinionSplit(t *testing.T) { require.NoError(t, err, "couldn't decode genesis seed from base58 seed") const genesisBalance = 1000000000 - genesisWallet := wallet.New(genesisSeedBytes) - genesisAddr := genesisWallet.Seed().Address(0) + genesisSeed := walletseed.NewSeed(genesisSeedBytes) + genesisAddr := genesisSeed.Address(0).Address genesisOutputID := transaction.NewOutputID(genesisAddr, transaction.GenesisID) // issue transactions which spend the same genesis output in all partitions conflictingTxs := make([]*transaction.Transaction, len(n.Partitions())) conflictingTxIDs := make([]string, len(n.Partitions())) - receiverWallets := make([]*wallet.Wallet, len(n.Partitions())) + receiverSeeds := make([]*walletseed.Seed, len(n.Partitions())) for i, partition := range n.Partitions() { // create a new receiver wallet for the given partition - partitionReceiverWallet := wallet.New() - destAddr := partitionReceiverWallet.Seed().Address(0) - receiverWallets[i] = partitionReceiverWallet + partitionReceiverSeed := walletseed.NewSeed() + destAddr := partitionReceiverSeed.Address(0).Address + receiverSeeds[i] = partitionReceiverSeed tx := transaction.New( transaction.NewInputs(genesisOutputID), transaction.NewOutputs(map[address.Address][]*balance.Balance{ @@ -78,7 +78,7 @@ func TestConsensusFiftyFiftyOpinionSplit(t *testing.T) { {Value: genesisBalance, Color: balance.ColorIOTA}, }, })) - tx = tx.Sign(signaturescheme.ED25519(*genesisWallet.Seed().KeyPair(0))) + tx = tx.Sign(signaturescheme.ED25519(*genesisSeed.KeyPair(0))) conflictingTxs[i] = tx // issue the transaction on the first peer of the partition diff --git a/tools/integration-tests/tester/tests/consensus/consensus_noconflicts_test.go b/tools/integration-tests/tester/tests/consensus/consensus_noconflicts_test.go index badf4040180e3135368f021125eca471be2a8f19..efb5bde599bc1ba7228165272f2058c8d9a1c55c 100644 --- a/tools/integration-tests/tester/tests/consensus/consensus_noconflicts_test.go +++ b/tools/integration-tests/tester/tests/consensus/consensus_noconflicts_test.go @@ -6,12 +6,12 @@ import ( "testing" "time" + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" "github.com/iotaledger/goshimmer/dapps/valuetransfers" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address/signaturescheme" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" "github.com/iotaledger/goshimmer/plugins/webapi/value/utils" "github.com/iotaledger/goshimmer/tools/integration-tests/tester/tests" "github.com/mr-tron/base58/base58" @@ -32,11 +32,11 @@ func TestConsensusNoConflicts(t *testing.T) { require.NoError(t, err, "couldn't decode genesis seed from base58 seed") const genesisBalance = 1000000000 - genesisWallet := wallet.New(genesisSeedBytes) - genesisAddr := genesisWallet.Seed().Address(0) + genesisSeed := walletseed.NewSeed(genesisSeedBytes) + genesisAddr := genesisSeed.Address(0).Address genesisOutputID := transaction.NewOutputID(genesisAddr, transaction.GenesisID) - firstReceiver := wallet.New() + firstReceiver := walletseed.NewSeed() const depositCount = 10 const deposit = genesisBalance / depositCount firstReceiverAddresses := make([]string, depositCount) @@ -44,7 +44,7 @@ func TestConsensusNoConflicts(t *testing.T) { firstReceiverDepositOutputs := map[address.Address][]*balance.Balance{} firstReceiverExpectedBalances := map[string]map[balance.Color]int64{} for i := 0; i < depositCount; i++ { - addr := firstReceiver.Seed().Address(uint64(i)) + addr := firstReceiver.Address(uint64(i)).Address firstReceiverDepositAddrs[i] = addr firstReceiverAddresses[i] = addr.String() firstReceiverDepositOutputs[addr] = []*balance.Balance{{Value: deposit, Color: balance.ColorIOTA}} @@ -54,7 +54,7 @@ func TestConsensusNoConflicts(t *testing.T) { // issue transaction spending from the genesis output log.Printf("issuing transaction spending genesis to %d addresses", depositCount) tx := transaction.New(transaction.NewInputs(genesisOutputID), transaction.NewOutputs(firstReceiverDepositOutputs)) - tx = tx.Sign(signaturescheme.ED25519(*genesisWallet.Seed().KeyPair(0))) + tx = tx.Sign(signaturescheme.ED25519(*genesisSeed.KeyPair(0))) utilsTx := utils.ParseTransaction(tx) txID, err := n.Peers()[0].SendTransaction(tx.Bytes()) @@ -86,20 +86,20 @@ func TestConsensusNoConflicts(t *testing.T) { tests.CheckBalances(t, n.Peers(), firstReceiverExpectedBalances) // issue transactions spending all the outputs which were just created from a random peer - secondReceiverWallet := wallet.New() + secondReceiverSeed := walletseed.NewSeed() secondReceiverAddresses := make([]string, depositCount) secondReceiverExpectedBalances := map[string]map[balance.Color]int64{} secondReceiverExpectedTransactions := map[string]*tests.ExpectedTransaction{} for i := 0; i < depositCount; i++ { - addr := secondReceiverWallet.Seed().Address(uint64(i)) + addr := secondReceiverSeed.Address(uint64(i)).Address tx := transaction.New( - transaction.NewInputs(transaction.NewOutputID(firstReceiver.Seed().Address(uint64(i)), tx.ID())), + transaction.NewInputs(transaction.NewOutputID(firstReceiver.Address(uint64(i)).Address, tx.ID())), transaction.NewOutputs(map[address.Address][]*balance.Balance{ addr: {{Value: deposit, Color: balance.ColorIOTA}}, }), ) secondReceiverAddresses[i] = addr.String() - tx = tx.Sign(signaturescheme.ED25519(*firstReceiver.Seed().KeyPair(uint64(i)))) + tx = tx.Sign(signaturescheme.ED25519(*firstReceiver.KeyPair(uint64(i)))) txID, err := n.Peers()[rand.Intn(len(n.Peers()))].SendTransaction(tx.Bytes()) require.NoError(t, err) diff --git a/tools/integration-tests/tester/tests/testutil.go b/tools/integration-tests/tester/tests/testutil.go index 49ab041eee8bae05bfc6e73256aef7cc5bd0a6b3..d6b8c98bd62811ec5fd4e86e8b82bd5da0810ae7 100644 --- a/tools/integration-tests/tester/tests/testutil.go +++ b/tools/integration-tests/tester/tests/testutil.go @@ -83,7 +83,7 @@ func SendFaucetRequestOnRandomPeer(t *testing.T, peers []*framework.Peer, numMes for i := 0; i < numMessages; i++ { peer := peers[rand.Intn(len(peers))] - addr := peer.Seed().Address(uint64(i)) + addr := peer.Seed.Address(uint64(i)).Address id, sent := SendFaucetRequest(t, peer, addr) ids[id] = sent addrBalance[addr.String()] = map[balance.Color]int64{ @@ -150,12 +150,12 @@ func SendTransactionFromFaucet(t *testing.T, peers []*framework.Peer, sentValue // initiate addrBalance map addrBalance = make(map[string]map[balance.Color]int64) for _, p := range peers { - addr := p.Seed().Address(0).String() + addr := p.Seed.Address(0).String() addrBalance[addr] = make(map[balance.Color]int64) } faucetPeer := peers[0] - faucetAddrStr := faucetPeer.Seed().Address(0).String() + faucetAddrStr := faucetPeer.Seed.Address(0).String() // get faucet balances unspentOutputs, err := faucetPeer.GetUnspentOutputs([]string{faucetAddrStr}) @@ -204,9 +204,9 @@ func SendTransactionOnRandomPeer(t *testing.T, peers []*framework.Peer, addrBala // SendIotaTransaction sends sentValue amount of IOTA tokens and remainders from and to a given peer and returns the fail flag and the transaction ID. // Every peer sends and receives the transaction on the address of index 0. func SendIotaTransaction(t *testing.T, from *framework.Peer, to *framework.Peer, addrBalance map[string]map[balance.Color]int64, sentValue int64) (fail bool, txId string) { - sigScheme := signaturescheme.ED25519(*from.Seed().KeyPair(0)) - inputAddr := from.Seed().Address(0) - outputAddr := to.Seed().Address(0) + sigScheme := signaturescheme.ED25519(*from.Seed.KeyPair(0)) + inputAddr := from.Seed.Address(0).Address + outputAddr := to.Seed.Address(0).Address // prepare inputs resp, err := from.GetUnspentOutputs([]string{inputAddr.String()}) @@ -288,9 +288,9 @@ func SendColoredTransactionOnRandomPeer(t *testing.T, peers []*framework.Peer, a func SendColoredTransaction(t *testing.T, from *framework.Peer, to *framework.Peer, addrBalance map[string]map[balance.Color]int64) (fail bool, txId string) { var sentValue int64 = 50 var balanceList []*balance.Balance - sigScheme := signaturescheme.ED25519(*from.Seed().KeyPair(0)) - inputAddr := from.Seed().Address(0) - outputAddr := to.Seed().Address(0) + sigScheme := signaturescheme.ED25519(*from.Seed.KeyPair(0)) + inputAddr := from.Seed.Address(0).Address + outputAddr := to.Seed.Address(0).Address // prepare inputs resp, err := from.GetUnspentOutputs([]string{inputAddr.String()}) diff --git a/tools/rand-address/main.go b/tools/rand-address/main.go index dd13cc419882fb2f1f7408dd9db1462f062b2f79..fb4c06c20c51616ca0c9ab8c796ab898d3563f60 100644 --- a/tools/rand-address/main.go +++ b/tools/rand-address/main.go @@ -3,9 +3,9 @@ package main import ( "fmt" - "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/wallet" + walletseed "github.com/iotaledger/goshimmer/client/wallet/packages/seed" ) func main() { - fmt.Println(wallet.New().Seed().Address(0)) + fmt.Println(walletseed.NewSeed().Address(0)) }