From d89192fa5df49d02533810e5b9cd2a01ebc19b38 Mon Sep 17 00:00:00 2001 From: Felipe Madero Date: Fri, 31 May 2024 16:29:04 -0400 Subject: [PATCH] mv commit/sign to wallet --- keychain/keychain.go | 16 +- ledger/ledger.go | 28 ++- multisig/multisig.go | 486 ++++++++++++++++++------------------------- wallet/wallet.go | 142 +++++++++++++ 4 files changed, 391 insertions(+), 281 deletions(-) diff --git a/keychain/keychain.go b/keychain/keychain.go index f66b056..728a039 100644 --- a/keychain/keychain.go +++ b/keychain/keychain.go @@ -10,6 +10,7 @@ import ( "github.com/ava-labs/avalanche-tooling-sdk-go/ledger" "github.com/ava-labs/avalanche-tooling-sdk-go/utils" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/keychain" "golang.org/x/exp/maps" @@ -44,7 +45,18 @@ func (kc *Keychain) AddLedgerIndices(indices []uint32) error { return fmt.Errorf("keychain is not ledger enabled") } -func (kc *Keychain) AddLedgerAddresses(addresses []string) error { +func (kc *Keychain) AddLedgerStrAddresses(addresses []string) error { + if kc.LedgerEnabled() { + indices, err := kc.ledgerDevice.FindStrAddresses(addresses, 0) + if err != nil { + return err + } + return kc.AddLedgerIndices(maps.Values(indices)) + } + return fmt.Errorf("keychain is not ledger enabled") +} + +func (kc *Keychain) AddLedgerAddresses(addresses []ids.ShortID) error { if kc.LedgerEnabled() { indices, err := kc.ledgerDevice.FindAddresses(addresses, 0) if err != nil { @@ -93,7 +105,7 @@ func NewKeychain( } } if len(ledgerAddresses) > 0 { - if err := kc.AddLedgerAddresses(ledgerAddresses); err != nil { + if err := kc.AddLedgerStrAddresses(ledgerAddresses); err != nil { return nil, err } } diff --git a/ledger/ledger.go b/ledger/ledger.go index 2e95539..0042cf9 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -8,6 +8,7 @@ import ( "github.com/ava-labs/avalanche-tooling-sdk-go/avalanche" "github.com/ava-labs/avalanche-tooling-sdk-go/utils" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/keychain" "github.com/ava-labs/avalanchego/utils/crypto/ledger" "github.com/ava-labs/avalanchego/utils/formatting/address" @@ -42,7 +43,7 @@ func (dev *LedgerDevice) P(network avalanche.Network, indices []uint32) ([]strin return utils.P(network.HRP(), addresses) } -func (dev *LedgerDevice) FindAddresses(addresses []string, maxIndex uint32) (map[string]uint32, error) { +func (dev *LedgerDevice) FindStrAddresses(addresses []string, maxIndex uint32) (map[string]uint32, error) { addressesIDs, err := address.ParseToIDs(addresses) if err != nil { return nil, fmt.Errorf("failure parsing ledger addresses: %w", err) @@ -71,6 +72,31 @@ func (dev *LedgerDevice) FindAddresses(addresses []string, maxIndex uint32) (map return indices, nil } +func (dev *LedgerDevice) FindAddresses(addresses []ids.ShortID, maxIndex uint32) (map[ids.ShortID]uint32, error) { + // for all ledger indices to search for, find if the ledger address belongs to the input + // addresses and, if so, add an index association to indexMap. + // breaks the loop if all addresses were found + if maxIndex == 0 { + maxIndex = maxIndexToSearch + } + indices := map[ids.ShortID]uint32{} + for index := uint32(0); index < maxIndex; index++ { + ledgerAddress, err := dev.Addresses([]uint32{index}) + if err != nil { + return nil, err + } + for addressIndex, addr := range addresses { + if addr == ledgerAddress[0] { + indices[addresses[addressIndex]] = index + } + } + if len(indices) == len(addresses) { + break + } + } + return indices, nil +} + // search for a set of indices that pay a given amount func (dev *LedgerDevice) FindFunds( network avalanche.Network, diff --git a/multisig/multisig.go b/multisig/multisig.go index ccc5e5b..8415225 100644 --- a/multisig/multisig.go +++ b/multisig/multisig.go @@ -3,20 +3,14 @@ package multisig import ( - "context" - "errors" "fmt" "os" - "time" "github.com/ava-labs/avalanche-tooling-sdk-go/avalanche" - "github.com/ava-labs/avalanche-tooling-sdk-go/utils" - "github.com/ava-labs/avalanche-tooling-sdk-go/wallet" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/formatting" "github.com/ava-labs/avalanchego/vms/components/verify" "github.com/ava-labs/avalanchego/vms/secp256k1fx" - "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/vms/platformvm/txs" @@ -24,6 +18,8 @@ import ( type TxKind int64 +var ErrUndefinedTx = fmt.Errorf("tx is undefined") + const ( Undefined TxKind = iota PChainRemoveSubnetValidatorTx @@ -40,8 +36,6 @@ type Multisig struct { threshold uint32 } -var ErrNoRemainingAuthSignersInWallet = errors.New("wallet does not contain any renaububg auth signer") - func New(pChainTx *txs.Tx) *Multisig { ms := Multisig{ pChainTx: pChainTx, @@ -56,39 +50,43 @@ func (ms *Multisig) String() string { return "" } +func (ms *Multisig) Undefined() bool { + return ms.pChainTx == nil +} + func (ms *Multisig) ToBytes() ([]byte, error) { - if ms.pChainTx != nil { - txBytes, err := txs.Codec.Marshal(txs.CodecVersion, ms.pChainTx) - if err != nil { - return nil, fmt.Errorf("couldn't marshal signed tx: %w", err) - } - return txBytes, nil + if ms.Undefined() { + return nil, ErrUndefinedTx } - return nil, fmt.Errorf("undefined tx") + txBytes, err := txs.Codec.Marshal(txs.CodecVersion, ms.pChainTx) + if err != nil { + return nil, fmt.Errorf("couldn't marshal signed tx: %w", err) + } + return txBytes, nil } func (ms *Multisig) ToFile(txPath string) error { - if ms.pChainTx != nil { - txBytes, err := ms.ToBytes() - if err != nil { - return err - } - txStr, err := formatting.Encode(formatting.Hex, txBytes) - if err != nil { - return fmt.Errorf("couldn't encode signed tx: %w", err) - } - f, err := os.Create(txPath) - if err != nil { - return fmt.Errorf("couldn't create file to write tx to: %w", err) - } - defer f.Close() - _, err = f.WriteString(txStr) - if err != nil { - return fmt.Errorf("couldn't write tx into file: %w", err) - } - return nil + if ms.Undefined() { + return ErrUndefinedTx + } + txBytes, err := ms.ToBytes() + if err != nil { + return err + } + txStr, err := formatting.Encode(formatting.Hex, txBytes) + if err != nil { + return fmt.Errorf("couldn't encode signed tx: %w", err) + } + f, err := os.Create(txPath) + if err != nil { + return fmt.Errorf("couldn't create file to write tx to: %w", err) + } + defer f.Close() + _, err = f.WriteString(txStr) + if err != nil { + return fmt.Errorf("couldn't write tx into file: %w", err) } - return fmt.Errorf("undefined tx") + return nil } func (ms *Multisig) FromBytes(txBytes []byte) error { @@ -115,44 +113,15 @@ func (ms *Multisig) FromFile(txPath string) error { return ms.FromBytes(txBytes) } -func (ms *Multisig) Commit(wallet wallet.Wallet, waitForTxAcceptance bool) (ids.ID, error) { - const ( - repeats = 3 - sleepBetweenRepeats = 2 * time.Second - ) - var issueTxErr error - for i := 0; i < repeats; i++ { - ctx, cancel := utils.GetAPILargeContext() - defer cancel() - options := []common.Option{common.WithContext(ctx)} - if !waitForTxAcceptance { - options = append(options, common.WithAssumeDecided()) - } - // TODO: split error checking and recovery between issuing and waiting for status - issueTxErr = wallet.P().IssueTx(ms.pChainTx, options...) - if issueTxErr == nil { - break - } - if ctx.Err() != nil { - issueTxErr = fmt.Errorf("timeout issuing/verifying tx with ID %s: %w", ms.pChainTx.ID(), issueTxErr) - } else { - issueTxErr = fmt.Errorf("error issuing tx with ID %s: %w", ms.pChainTx.ID(), issueTxErr) - } - time.Sleep(sleepBetweenRepeats) - } - // TODO: having a commit error, maybe should be useful to reestart the wallet internal info - return ms.pChainTx.ID(), issueTxErr -} - func (ms *Multisig) IsReadyToCommit() (bool, error) { - if ms.pChainTx != nil { - _, remainingSigners, err := ms.GetRemainingAuthSigners() - if err != nil { - return false, err - } - return len(remainingSigners) == 0, nil + if ms.Undefined() { + return false, ErrUndefinedTx + } + _, remainingSigners, err := ms.GetRemainingAuthSigners() + if err != nil { + return false, err } - return false, nil + return len(remainingSigners) == 0, nil } // get subnet auth addresses that did not yet signed a given tx @@ -165,48 +134,48 @@ func (ms *Multisig) IsReadyToCommit() (bool, error) { // // if the tx is fully signed, returns empty slice func (ms *Multisig) GetRemainingAuthSigners() ([]ids.ShortID, []ids.ShortID, error) { - if ms.pChainTx != nil { - authSigners, err := ms.GetAuthSigners() - if err != nil { - return nil, nil, err - } - emptySig := [secp256k1.SignatureLen]byte{} - // we should have at least 1 cred for output owners and 1 cred for subnet auth - if len(ms.pChainTx.Creds) < 2 { - return nil, nil, fmt.Errorf("expected tx.Creds of len 2, got %d", len(ms.pChainTx.Creds)) - } - // signatures for output owners should be filled (all creds except last one) - for credIndex := range ms.pChainTx.Creds[:len(ms.pChainTx.Creds)-1] { - cred, ok := ms.pChainTx.Creds[credIndex].(*secp256k1fx.Credential) - if !ok { - return nil, nil, fmt.Errorf("expected cred to be of type *secp256k1fx.Credential, got %T", ms.pChainTx.Creds[credIndex]) - } - for i, sig := range cred.Sigs { - if sig == emptySig { - return nil, nil, fmt.Errorf("expected funding sig %d of cred %d to be filled", i, credIndex) - } - } - } - // signatures for subnet auth (last cred) - cred, ok := ms.pChainTx.Creds[len(ms.pChainTx.Creds)-1].(*secp256k1fx.Credential) + if ms.Undefined() { + return nil, nil, ErrUndefinedTx + } + authSigners, err := ms.GetAuthSigners() + if err != nil { + return nil, nil, err + } + emptySig := [secp256k1.SignatureLen]byte{} + // we should have at least 1 cred for output owners and 1 cred for subnet auth + if len(ms.pChainTx.Creds) < 2 { + return nil, nil, fmt.Errorf("expected tx.Creds of len 2, got %d", len(ms.pChainTx.Creds)) + } + // signatures for output owners should be filled (all creds except last one) + for credIndex := range ms.pChainTx.Creds[:len(ms.pChainTx.Creds)-1] { + cred, ok := ms.pChainTx.Creds[credIndex].(*secp256k1fx.Credential) if !ok { - return nil, nil, fmt.Errorf("expected cred to be of type *secp256k1fx.Credential, got %T", ms.pChainTx.Creds[1]) - } - if len(cred.Sigs) != len(authSigners) { - return nil, nil, fmt.Errorf("expected number of cred's signatures %d to equal number of auth signers %d", - len(cred.Sigs), - len(authSigners), - ) + return nil, nil, fmt.Errorf("expected cred to be of type *secp256k1fx.Credential, got %T", ms.pChainTx.Creds[credIndex]) } - remainingSigners := []ids.ShortID{} for i, sig := range cred.Sigs { if sig == emptySig { - remainingSigners = append(remainingSigners, authSigners[i]) + return nil, nil, fmt.Errorf("expected funding sig %d of cred %d to be filled", i, credIndex) } } - return authSigners, remainingSigners, nil } - return nil, nil, fmt.Errorf("undefined tx") + // signatures for subnet auth (last cred) + cred, ok := ms.pChainTx.Creds[len(ms.pChainTx.Creds)-1].(*secp256k1fx.Credential) + if !ok { + return nil, nil, fmt.Errorf("expected cred to be of type *secp256k1fx.Credential, got %T", ms.pChainTx.Creds[1]) + } + if len(cred.Sigs) != len(authSigners) { + return nil, nil, fmt.Errorf("expected number of cred's signatures %d to equal number of auth signers %d", + len(cred.Sigs), + len(authSigners), + ) + } + remainingSigners := []ids.ShortID{} + for i, sig := range cred.Sigs { + if sig == emptySig { + remainingSigners = append(remainingSigners, authSigners[i]) + } + } + return authSigners, remainingSigners, nil } // get all subnet auth addresses that are required to sign a given tx @@ -215,41 +184,41 @@ func (ms *Multisig) GetRemainingAuthSigners() ([]ids.ShortID, []ids.ShortID, err // - creates the string slice of required subnet auth addresses by applying // the indices to the control keys slice func (ms *Multisig) GetAuthSigners() ([]ids.ShortID, error) { - if ms.pChainTx != nil { - controlKeys, _, err := ms.GetSubnetOwners() - if err != nil { - return nil, err - } - unsignedTx := ms.pChainTx.Unsigned - var subnetAuth verify.Verifiable - switch unsignedTx := unsignedTx.(type) { - case *txs.RemoveSubnetValidatorTx: - subnetAuth = unsignedTx.SubnetAuth - case *txs.AddSubnetValidatorTx: - subnetAuth = unsignedTx.SubnetAuth - case *txs.CreateChainTx: - subnetAuth = unsignedTx.SubnetAuth - case *txs.TransformSubnetTx: - subnetAuth = unsignedTx.SubnetAuth - case *txs.TransferSubnetOwnershipTx: - subnetAuth = unsignedTx.SubnetAuth - default: - return nil, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) - } - subnetInput, ok := subnetAuth.(*secp256k1fx.Input) - if !ok { - return nil, fmt.Errorf("expected subnetAuth of type *secp256k1fx.Input, got %T", subnetAuth) - } - authSigners := []ids.ShortID{} - for _, sigIndex := range subnetInput.SigIndices { - if sigIndex >= uint32(len(controlKeys)) { - return nil, fmt.Errorf("signer index %d exceeds number of control keys", sigIndex) - } - authSigners = append(authSigners, controlKeys[sigIndex]) + if ms.Undefined() { + return nil, ErrUndefinedTx + } + controlKeys, _, err := ms.GetSubnetOwners() + if err != nil { + return nil, err + } + unsignedTx := ms.pChainTx.Unsigned + var subnetAuth verify.Verifiable + switch unsignedTx := unsignedTx.(type) { + case *txs.RemoveSubnetValidatorTx: + subnetAuth = unsignedTx.SubnetAuth + case *txs.AddSubnetValidatorTx: + subnetAuth = unsignedTx.SubnetAuth + case *txs.CreateChainTx: + subnetAuth = unsignedTx.SubnetAuth + case *txs.TransformSubnetTx: + subnetAuth = unsignedTx.SubnetAuth + case *txs.TransferSubnetOwnershipTx: + subnetAuth = unsignedTx.SubnetAuth + default: + return nil, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) + } + subnetInput, ok := subnetAuth.(*secp256k1fx.Input) + if !ok { + return nil, fmt.Errorf("expected subnetAuth of type *secp256k1fx.Input, got %T", subnetAuth) + } + authSigners := []ids.ShortID{} + for _, sigIndex := range subnetInput.SigIndices { + if sigIndex >= uint32(len(controlKeys)) { + return nil, fmt.Errorf("signer index %d exceeds number of control keys", sigIndex) } - return authSigners, nil + authSigners = append(authSigners, controlKeys[sigIndex]) } - return nil, fmt.Errorf("undefined tx") + return authSigners, nil } func (*Multisig) GetSpendSigners() ([]ids.ShortID, error) { @@ -257,122 +226,125 @@ func (*Multisig) GetSpendSigners() ([]ids.ShortID, error) { } func (ms *Multisig) GetTxKind() (TxKind, error) { - if ms.pChainTx != nil { - unsignedTx := ms.pChainTx.Unsigned - switch unsignedTx := unsignedTx.(type) { - case *txs.RemoveSubnetValidatorTx: - return PChainRemoveSubnetValidatorTx, nil - case *txs.AddSubnetValidatorTx: - return PChainAddSubnetValidatorTx, nil - case *txs.CreateChainTx: - return PChainCreateChainTx, nil - case *txs.TransformSubnetTx: - return PChainTransformSubnetTx, nil - case *txs.AddPermissionlessValidatorTx: - return PChainAddPermissionlessValidatorTx, nil - case *txs.TransferSubnetOwnershipTx: - return PChainTransferSubnetOwnershipTx, nil - default: - return Undefined, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) - } + if ms.Undefined() { + return Undefined, ErrUndefinedTx + } + unsignedTx := ms.pChainTx.Unsigned + switch unsignedTx := unsignedTx.(type) { + case *txs.RemoveSubnetValidatorTx: + return PChainRemoveSubnetValidatorTx, nil + case *txs.AddSubnetValidatorTx: + return PChainAddSubnetValidatorTx, nil + case *txs.CreateChainTx: + return PChainCreateChainTx, nil + case *txs.TransformSubnetTx: + return PChainTransformSubnetTx, nil + case *txs.AddPermissionlessValidatorTx: + return PChainAddPermissionlessValidatorTx, nil + case *txs.TransferSubnetOwnershipTx: + return PChainTransferSubnetOwnershipTx, nil + default: + return Undefined, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) } - return Undefined, fmt.Errorf("undefined tx") } // get network id associated to tx func (ms *Multisig) GetNetworkID() (uint32, error) { - if ms.pChainTx != nil { - unsignedTx := ms.pChainTx.Unsigned - var networkID uint32 - switch unsignedTx := unsignedTx.(type) { - case *txs.RemoveSubnetValidatorTx: - networkID = unsignedTx.NetworkID - case *txs.AddSubnetValidatorTx: - networkID = unsignedTx.NetworkID - case *txs.CreateChainTx: - networkID = unsignedTx.NetworkID - case *txs.TransformSubnetTx: - networkID = unsignedTx.NetworkID - case *txs.AddPermissionlessValidatorTx: - networkID = unsignedTx.NetworkID - case *txs.TransferSubnetOwnershipTx: - networkID = unsignedTx.NetworkID - default: - return 0, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) - } - return networkID, nil + if ms.Undefined() { + return 0, ErrUndefinedTx + } + unsignedTx := ms.pChainTx.Unsigned + var networkID uint32 + switch unsignedTx := unsignedTx.(type) { + case *txs.RemoveSubnetValidatorTx: + networkID = unsignedTx.NetworkID + case *txs.AddSubnetValidatorTx: + networkID = unsignedTx.NetworkID + case *txs.CreateChainTx: + networkID = unsignedTx.NetworkID + case *txs.TransformSubnetTx: + networkID = unsignedTx.NetworkID + case *txs.AddPermissionlessValidatorTx: + networkID = unsignedTx.NetworkID + case *txs.TransferSubnetOwnershipTx: + networkID = unsignedTx.NetworkID + default: + return 0, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) } - return 0, fmt.Errorf("undefined tx") + return networkID, nil } // get network model associated to tx func (ms *Multisig) GetNetwork() (avalanche.Network, error) { - if ms.pChainTx != nil { - networkID, err := ms.GetNetworkID() - if err != nil { - return avalanche.UndefinedNetwork, err - } - network := avalanche.NetworkFromNetworkID(networkID) - if network.Kind == avalanche.Undefined { - return avalanche.UndefinedNetwork, fmt.Errorf("undefined network model for tx") - } - return network, nil + if ms.Undefined() { + return avalanche.UndefinedNetwork, ErrUndefinedTx } - return avalanche.Network{}, fmt.Errorf("undefined tx") + networkID, err := ms.GetNetworkID() + if err != nil { + return avalanche.UndefinedNetwork, err + } + network := avalanche.NetworkFromNetworkID(networkID) + if network.Kind == avalanche.Undefined { + return avalanche.UndefinedNetwork, fmt.Errorf("undefined network model for tx") + } + return network, nil } func (ms *Multisig) GetBlockchainID() (ids.ID, error) { - if ms.pChainTx != nil { - unsignedTx := ms.pChainTx.Unsigned - var blockchainID ids.ID - switch unsignedTx := unsignedTx.(type) { - case *txs.RemoveSubnetValidatorTx: - blockchainID = unsignedTx.BlockchainID - case *txs.AddSubnetValidatorTx: - blockchainID = unsignedTx.BlockchainID - case *txs.CreateChainTx: - blockchainID = unsignedTx.BlockchainID - case *txs.TransformSubnetTx: - blockchainID = unsignedTx.BlockchainID - case *txs.AddPermissionlessValidatorTx: - blockchainID = unsignedTx.BlockchainID - case *txs.TransferSubnetOwnershipTx: - blockchainID = unsignedTx.BlockchainID - default: - return ids.Empty, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) - } - return blockchainID, nil + if ms.Undefined() { + return ids.Empty, ErrUndefinedTx + } + unsignedTx := ms.pChainTx.Unsigned + var blockchainID ids.ID + switch unsignedTx := unsignedTx.(type) { + case *txs.RemoveSubnetValidatorTx: + blockchainID = unsignedTx.BlockchainID + case *txs.AddSubnetValidatorTx: + blockchainID = unsignedTx.BlockchainID + case *txs.CreateChainTx: + blockchainID = unsignedTx.BlockchainID + case *txs.TransformSubnetTx: + blockchainID = unsignedTx.BlockchainID + case *txs.AddPermissionlessValidatorTx: + blockchainID = unsignedTx.BlockchainID + case *txs.TransferSubnetOwnershipTx: + blockchainID = unsignedTx.BlockchainID + default: + return ids.Empty, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) } - return ids.Empty, fmt.Errorf("undefined tx") + return blockchainID, nil } // get subnet id associated to tx func (ms *Multisig) GetSubnetID() (ids.ID, error) { - if ms.pChainTx != nil { - unsignedTx := ms.pChainTx.Unsigned - var subnetID ids.ID - switch unsignedTx := unsignedTx.(type) { - case *txs.RemoveSubnetValidatorTx: - subnetID = unsignedTx.Subnet - case *txs.AddSubnetValidatorTx: - subnetID = unsignedTx.SubnetValidator.Subnet - case *txs.CreateChainTx: - subnetID = unsignedTx.SubnetID - case *txs.TransformSubnetTx: - subnetID = unsignedTx.Subnet - case *txs.AddPermissionlessValidatorTx: - subnetID = unsignedTx.Subnet - case *txs.TransferSubnetOwnershipTx: - subnetID = unsignedTx.Subnet - default: - return ids.Empty, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) - } - return subnetID, nil + if ms.Undefined() { + return ids.Empty, ErrUndefinedTx + } + unsignedTx := ms.pChainTx.Unsigned + var subnetID ids.ID + switch unsignedTx := unsignedTx.(type) { + case *txs.RemoveSubnetValidatorTx: + subnetID = unsignedTx.Subnet + case *txs.AddSubnetValidatorTx: + subnetID = unsignedTx.SubnetValidator.Subnet + case *txs.CreateChainTx: + subnetID = unsignedTx.SubnetID + case *txs.TransformSubnetTx: + subnetID = unsignedTx.Subnet + case *txs.AddPermissionlessValidatorTx: + subnetID = unsignedTx.Subnet + case *txs.TransferSubnetOwnershipTx: + subnetID = unsignedTx.Subnet + default: + return ids.Empty, fmt.Errorf("unexpected unsigned tx type %T", unsignedTx) } - return ids.Empty, fmt.Errorf("undefined tx") + return subnetID, nil } func (ms *Multisig) GetSubnetOwners() ([]ids.ShortID, uint32, error) { + if ms.Undefined() { + return nil, 0, ErrUndefinedTx + } if ms.controlKeys == nil { subnetID, err := ms.GetSubnetID() if err != nil { @@ -396,51 +368,9 @@ func GetOwners(_ avalanche.Network, _ ids.ID) ([]ids.ShortID, uint32, error) { return nil, 0, fmt.Errorf("not implemented") } -func (ms *Multisig) Sign( - wallet wallet.Wallet, - checkAuth bool, - commitIfReady bool, - waitForTxAcceptanceOnCommit bool, -) (bool, bool, ids.ID, error) { - if ms.pChainTx != nil { - if checkAuth { - remainingInWallet, err := ms.GetRemainingAuthSignersInWallet(wallet) - if err != nil { - return false, false, ids.Empty, fmt.Errorf("error signing tx: %w", err) - } - if len(remainingInWallet) == 0 { - return false, false, ids.Empty, ErrNoRemainingAuthSignersInWallet - } - } - if err := wallet.P().Signer().Sign(context.Background(), ms.pChainTx); err != nil { - return false, false, ids.Empty, fmt.Errorf("error signing tx: %w", err) - } - isReady, err := ms.IsReadyToCommit() - if err != nil { - return false, false, ids.Empty, err - } - if commitIfReady && isReady { - _, err := ms.Commit(wallet, waitForTxAcceptanceOnCommit) - return isReady, err == nil, ms.pChainTx.ID(), err - } - return isReady, false, ms.pChainTx.ID(), nil - } - return false, false, ids.Empty, fmt.Errorf("undefined tx") -} - -func (ms *Multisig) GetRemainingAuthSignersInWallet(wallet wallet.Wallet) ([]ids.ShortID, error) { - _, subnetAuth, err := ms.GetRemainingAuthSigners() - if err != nil { - return nil, err - } - walletAddrs := wallet.Addresses() - subnetAuthInWallet := []ids.ShortID{} - for _, walletAddr := range walletAddrs { - for _, addr := range subnetAuth { - if addr == walletAddr { - subnetAuthInWallet = append(subnetAuthInWallet, addr) - } - } +func (ms *Multisig) GetWrappedPChainTx() (*txs.Tx, error) { + if ms.Undefined() { + return nil, ErrUndefinedTx } - return subnetAuthInWallet, nil + return ms.pChainTx, nil } diff --git a/wallet/wallet.go b/wallet/wallet.go index 42e3a84..64842f2 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -4,8 +4,13 @@ package wallet import ( "context" + "errors" + "fmt" + "time" "github.com/ava-labs/avalanche-tooling-sdk-go/keychain" + "github.com/ava-labs/avalanche-tooling-sdk-go/multisig" + "github.com/ava-labs/avalanche-tooling-sdk-go/utils" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/set" @@ -14,10 +19,16 @@ import ( "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) +var ( + ErrNoRemainingAuthSignersInWallet = errors.New("wallet does not contain any remaining auth signer") + ErrNotReadyToCommit = errors.New("tx is not fully signed so can't be commited") +) + type Wallet struct { primary.Wallet keychain keychain.Keychain options []common.Option + config *primary.WalletConfig } func New(ctx context.Context, config *primary.WalletConfig) (Wallet, error) { @@ -30,9 +41,24 @@ func New(ctx context.Context, config *primary.WalletConfig) (Wallet, error) { keychain: keychain.Keychain{ Keychain: config.AVAXKeychain, }, + config: config, }, err } +func (w *Wallet) ResetKeychain(ctx context.Context, kc *keychain.Keychain) error { + w.config.AVAXKeychain = kc.Keychain + wallet, err := primary.MakeWallet( + ctx, + w.config, + ) + if err != nil { + return err + } + w.Wallet = wallet + w.keychain = *kc + return nil +} + // SecureWalletIsChangeOwner ensures that a fee paying address (wallet's keychain) will receive // the change UTXO and not a randomly selected auth key that may not be paying fees func (w *Wallet) SecureWalletIsChangeOwner() { @@ -66,3 +92,119 @@ func (w *Wallet) SetSubnetAuthMultisig(authKeys []ids.ShortID) { func (w *Wallet) Addresses() []ids.ShortID { return w.keychain.Addresses().List() } + +func (w *Wallet) Sign( + ms multisig.Multisig, + checkAuth bool, + commitIfReady bool, + waitForTxAcceptanceOnCommit bool, +) (bool, bool, ids.ID, error) { + if ms.Undefined() { + return false, false, ids.Empty, multisig.ErrUndefinedTx + } + if w.keychain.LedgerEnabled() { + // let's see if there is we can find remaining subnet auth keys in the LedgerEnabled + _, remaining, err := ms.GetRemainingAuthSigners() + if err != nil { + return false, false, ids.Empty, err + } + kc := w.keychain + oldCount := len(kc.Addresses()) + err = kc.AddLedgerAddresses(remaining) + if err != nil { + return false, false, ids.Empty, err + } + if len(kc.Addresses()) != oldCount { + ctx, cancel := utils.GetAPIContext() + defer cancel() + if err := w.ResetKeychain(ctx, &kc); err != nil { + return false, false, ids.Empty, err + } + } + } + if checkAuth { + remainingInWallet, err := w.GetRemainingAuthSignersInWallet(ms) + if err != nil { + return false, false, ids.Empty, fmt.Errorf("error signing tx: %w", err) + } + if len(remainingInWallet) == 0 { + return false, false, ids.Empty, ErrNoRemainingAuthSignersInWallet + } + } + tx, err := ms.GetWrappedPChainTx() + if err != nil { + return false, false, ids.Empty, err + } + if err := w.P().Signer().Sign(context.Background(), tx); err != nil { + return false, false, ids.Empty, fmt.Errorf("error signing tx: %w", err) + } + isReady, err := ms.IsReadyToCommit() + if err != nil { + return false, false, ids.Empty, err + } + if commitIfReady && isReady { + _, err := w.Commit(ms, waitForTxAcceptanceOnCommit) + return isReady, err == nil, tx.ID(), err + } + return isReady, false, tx.ID(), nil +} + +func (w *Wallet) GetRemainingAuthSignersInWallet(ms multisig.Multisig) ([]ids.ShortID, error) { + _, subnetAuth, err := ms.GetRemainingAuthSigners() + if err != nil { + return nil, err + } + walletAddrs := w.Addresses() + subnetAuthInWallet := []ids.ShortID{} + for _, walletAddr := range walletAddrs { + for _, addr := range subnetAuth { + if addr == walletAddr { + subnetAuthInWallet = append(subnetAuthInWallet, addr) + } + } + } + return subnetAuthInWallet, nil +} + +func (w *Wallet) Commit(ms multisig.Multisig, waitForTxAcceptance bool) (ids.ID, error) { + if ms.Undefined() { + return ids.Empty, multisig.ErrUndefinedTx + } + isReady, err := ms.IsReadyToCommit() + if err != nil { + return ids.Empty, err + } + if !isReady { + return ids.Empty, ErrNotReadyToCommit + } + tx, err := ms.GetWrappedPChainTx() + if err != nil { + return ids.Empty, err + } + const ( + repeats = 3 + sleepBetweenRepeats = 2 * time.Second + ) + var issueTxErr error + for i := 0; i < repeats; i++ { + ctx, cancel := utils.GetAPILargeContext() + defer cancel() + options := []common.Option{common.WithContext(ctx)} + if !waitForTxAcceptance { + options = append(options, common.WithAssumeDecided()) + } + // TODO: split error checking and recovery between issuing and waiting for status + issueTxErr = w.P().IssueTx(tx, options...) + if issueTxErr == nil { + break + } + if ctx.Err() != nil { + issueTxErr = fmt.Errorf("timeout issuing/verifying tx with ID %s: %w", tx.ID(), issueTxErr) + } else { + issueTxErr = fmt.Errorf("error issuing tx with ID %s: %w", tx.ID(), issueTxErr) + } + time.Sleep(sleepBetweenRepeats) + } + // TODO: having a commit error, maybe should be useful to reestart the wallet internal info + return tx.ID(), issueTxErr +}