diff --git a/avalanche/keychain.go b/avalanche/keychain.go deleted file mode 100644 index fd1741c..0000000 --- a/avalanche/keychain.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package avalanche - -import "github.com/ava-labs/avalanchego/utils/crypto/keychain" - -type Keychain struct { - Network Network - - Keychain keychain.Keychain - - Ledger keychain.Ledger - - UsesLedger bool - - LedgerIndices []uint32 -} diff --git a/avalanche/network.go b/avalanche/network.go index c33fdf7..e354792 100644 --- a/avalanche/network.go +++ b/avalanche/network.go @@ -3,6 +3,8 @@ package avalanche +import "github.com/ava-labs/avalanchego/utils/constants" + type NetworkKind int64 const ( @@ -28,11 +30,22 @@ func (nk NetworkKind) String() string { } type Network struct { - Kind NetworkKind - - ID uint32 - + Kind NetworkKind + ID uint32 Endpoint string } var UndefinedNetwork = Network{} + +func (n Network) HRP() string { + switch n.ID { + case constants.LocalID: + return constants.LocalHRP + case constants.FujiID: + return constants.FujiHRP + case constants.MainnetID: + return constants.MainnetHRP + default: + return constants.FallbackHRP + } +} diff --git a/go.mod b/go.mod index 25c2a0a..aa19e2a 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,17 @@ require ( github.com/ava-labs/avalanchego v1.11.5 github.com/ava-labs/coreth v0.13.3-rc.2 github.com/ava-labs/subnet-evm v0.6.4 + golang.org/x/exp v0.0.0-20231127185646-65229373498e ) require ( github.com/DataDog/zstd v1.5.2 // indirect + github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect + github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/VictoriaMetrics/fastcache v1.10.0 // indirect + github.com/ava-labs/coreth v0.13.3-rc.2 // indirect + github.com/ava-labs/ledger-avalanche/go v0.0.0-20231102202641-ae2ebdaeac34 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.7.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect @@ -92,10 +97,13 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tyler-smith/go-bip32 v1.0.0 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/urfave/cli/v2 v2.25.7 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect + github.com/zondax/hid v0.9.2 // indirect + github.com/zondax/ledger-go v0.14.3 // indirect go.opentelemetry.io/otel v1.22.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.22.0 // indirect @@ -108,7 +116,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.21.0 // indirect - golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.18.0 // indirect diff --git a/go.sum b/go.sum index 6f95061..e65b219 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,10 @@ github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3 github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25YnZWly5Gq1ekg6jcmWaGj/vG/MhF4aisoc= +github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:kGUqhHd//musdITWjFvNTHn90WG9bMLBEPQZ17Cmlpw= +github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec h1:1Qb69mGp/UtRPn422BH4/Y4Q3SLUrD9KHuDkm8iodFc= +github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec/go.mod h1:CD8UlnlLDiqb36L110uqiP2iSflVjx9g/3U9hCI4q2U= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= @@ -24,6 +28,8 @@ github.com/ava-labs/coreth v0.13.3-rc.2 h1:lhyQwln6at1DTs1O586dMSAtGtSfQWlt2WH+Z github.com/ava-labs/coreth v0.13.3-rc.2/go.mod h1:4l15XGak3FklhIb7CtlC/1YVwGAfMl83R2zd2N0hNE0= github.com/ava-labs/subnet-evm v0.6.4 h1:iOz21dlwU/gTN7ZbD9lVeG9rIUt7MVWu1gnZlekl7nE= github.com/ava-labs/subnet-evm v0.6.4/go.mod h1:kv8NzG+N6hB4brfOE4a7laQNebNgru1sRcMS1OzXy+M= +github.com/ava-labs/ledger-avalanche/go v0.0.0-20231102202641-ae2ebdaeac34 h1:mg9Uw6oZFJKytJxgxnl3uxZOs/SB8CVHg6Io4Tf99Zc= +github.com/ava-labs/ledger-avalanche/go v0.0.0-20231102202641-ae2ebdaeac34/go.mod h1:pJxaT9bUgeRNVmNRgtCHb7sFDIRKy7CzTQVi8gGNT6g= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -68,6 +74,8 @@ github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86c github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw= +github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/datadriven v1.0.3-0.20230801171734-e384cf455877 h1:1MLK4YpFtIEo3ZtMA5C795Wtv5VuUnrXX7mQG+aHg6o= @@ -402,6 +410,7 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.1.5-0.20170601210322-f6abca593680/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -421,6 +430,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tyler-smith/go-bip32 v1.0.0 h1:sDR9juArbUgX+bO/iblgZnMPeWY1KZMUC2AFUJdv5KE= +github.com/tyler-smith/go-bip32 v1.0.0/go.mod h1:onot+eHknzV4BVPwrzqY5OoVpyCvnwD7lMawL5aQupE= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= @@ -451,6 +462,10 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= +github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= +github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4= @@ -475,6 +490,11 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20170613210332-850760c427c5/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -678,5 +698,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54= +launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/key/key.go b/key/key.go index b4ba703..b1b7bec 100644 --- a/key/key.go +++ b/key/key.go @@ -24,7 +24,7 @@ var ( // Key defines methods for key manager interface. type Key interface { // P returns all formatted P-Chain addresses. - P() []string + P(string) (string, error) // C returns the C-Chain address in Ethereum format C() string // Addresses returns the all raw ids.ShortID address. @@ -80,19 +80,6 @@ func WithFeeDeduct(fee uint64) OpOption { } } -func GetHRP(networkID uint32) string { - switch networkID { - case constants.LocalID: - return constants.LocalHRP - case constants.FujiID: - return constants.FujiHRP - case constants.MainnetID: - return constants.MainnetHRP - default: - return constants.FallbackHRP - } -} - type innerSortTransferableInputsWithSigners struct { ins []*avax.TransferableInput signers [][]ids.ShortID diff --git a/key/key_test.go b/key/key_test.go new file mode 100644 index 0000000..f7ce514 --- /dev/null +++ b/key/key_test.go @@ -0,0 +1,115 @@ +// Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package key + +import ( + "bytes" + "errors" + "path/filepath" + "testing" + + "github.com/ava-labs/avalanchego/utils/cb58" + "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" +) + +const ewoqPChainAddr = "P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p" + +func TestNewKeyEwoq(t *testing.T) { + t.Parallel() + + m, err := NewSoft( + WithPrivateKeyEncoded(EwoqPrivateKey), + ) + if err != nil { + t.Fatal(err) + } + + pAddr, err := m.P("custom") + if err != nil { + t.Fatal(err) + } + if pAddr != ewoqPChainAddr { + t.Fatalf("unexpected P-Chain address %q, expected %q", pAddr, ewoqPChainAddr) + } + + keyPath := filepath.Join(t.TempDir(), "key.pk") + if err := m.Save(keyPath); err != nil { + t.Fatal(err) + } + + m2, err := LoadSoft(keyPath) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(m.PrivKeyRaw(), m2.PrivKeyRaw()) { + t.Fatalf("loaded key unexpected %v, expected %v", m2.PrivKeyRaw(), m.PrivKeyRaw()) + } +} + +func TestNewKey(t *testing.T) { + t.Parallel() + + skBytes, err := cb58.Decode(rawEwoqPk) + if err != nil { + t.Fatal(err) + } + ewoqPk, err := secp256k1.ToPrivateKey(skBytes) + if err != nil { + t.Fatal(err) + } + + privKey2, err := secp256k1.NewPrivateKey() + if err != nil { + t.Fatal(err) + } + + tt := []struct { + name string + opts []SOpOption + expErr error + }{ + { + name: "test", + opts: nil, + expErr: nil, + }, + { + name: "ewop with WithPrivateKey", + opts: []SOpOption{ + WithPrivateKey(ewoqPk), + }, + expErr: nil, + }, + { + name: "ewop with WithPrivateKeyEncoded", + opts: []SOpOption{ + WithPrivateKeyEncoded(EwoqPrivateKey), + }, + expErr: nil, + }, + { + name: "ewop with WithPrivateKey/WithPrivateKeyEncoded", + opts: []SOpOption{ + WithPrivateKey(ewoqPk), + WithPrivateKeyEncoded(EwoqPrivateKey), + }, + expErr: nil, + }, + { + name: "ewop with invalid WithPrivateKey", + opts: []SOpOption{ + WithPrivateKey(privKey2), + WithPrivateKeyEncoded(EwoqPrivateKey), + }, + expErr: ErrInvalidPrivateKey, + }, + } + for i, tv := range tt { + _, err := NewSoft(tv.opts...) + if !errors.Is(err, tv.expErr) { + t.Fatalf("#%d(%s): unexpected error %v, expected %v", i, tv.name, err, tv.expErr) + } + } +} diff --git a/key/ledger_key.go b/key/ledger_key.go new file mode 100644 index 0000000..702ccfb --- /dev/null +++ b/key/ledger_key.go @@ -0,0 +1,78 @@ +// Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package key + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" +) + +var _ Key = &LedgerKey{} + +type LedgerKey struct { + index uint32 +} + +// ledger device should be connected +func NewLedger(index uint32) LedgerKey { + return LedgerKey{ + index: index, + } +} + +// LoadLedger loads the ledger key info from disk and creates the corresponding LedgerKey. +func LoadLedger(_ string) (*LedgerKey, error) { + return nil, fmt.Errorf("not implemented") +} + +// LoadLedgerFromBytes loads the ledger key info from bytes and creates the corresponding LedgerKey. +func LoadLedgerFromBytes(_ []byte) (*SoftKey, error) { + return nil, fmt.Errorf("not implemented") +} + +func (*LedgerKey) C() string { + return "" +} + +// Returns the KeyChain +func (*LedgerKey) KeyChain() *secp256k1fx.Keychain { + return nil +} + +// Saves the key info to disk +func (*LedgerKey) Save(_ string) error { + return fmt.Errorf("not implemented") +} + +func (*LedgerKey) P(_ string) (string, error) { + return "", fmt.Errorf("not implemented") +} + +func (*LedgerKey) X(_ string) (string, error) { + return "", fmt.Errorf("not implemented") +} + +func (*LedgerKey) Spends(_ []*avax.UTXO, _ ...OpOption) ( + totalBalanceToSpend uint64, + inputs []*avax.TransferableInput, + signers [][]ids.ShortID, +) { + return 0, nil, nil +} + +func (*LedgerKey) Addresses() []ids.ShortID { + return nil +} + +func (*LedgerKey) Sign(_ *txs.Tx, _ [][]ids.ShortID) error { + return fmt.Errorf("not implemented") +} + +func (*LedgerKey) Match(_ *secp256k1fx.OutputOwners, _ uint64) ([]uint32, []ids.ShortID, bool) { + return nil, nil, false +} diff --git a/key/soft_key.go b/key/soft_key.go index 4b0be0a..bb8dc53 100644 --- a/key/soft_key.go +++ b/key/soft_key.go @@ -11,6 +11,8 @@ import ( "io" "os" "strings" + + "avalanche-tooling-sdk-go/utils" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/cb58" @@ -38,9 +40,6 @@ type SoftKey struct { privKeyRaw []byte privKeyEncoded string - pAddr string - xAddr string - keyChain *secp256k1fx.Keychain } @@ -81,7 +80,7 @@ func WithPrivateKeyEncoded(privKey string) SOpOption { } } -func NewSoft(networkID uint32, opts ...SOpOption) (*SoftKey, error) { +func NewSoft(opts ...SOpOption) (*SoftKey, error) { ret := &SOp{} ret.applyOpts(opts) @@ -130,38 +129,42 @@ func NewSoft(networkID uint32, opts ...SOpOption) (*SoftKey, error) { keyChain: keyChain, } - - // Parse HRP to create valid address - hrp := GetHRP(networkID) - m.pAddr, err = address.Format("P", hrp, m.privKey.PublicKey().Address().Bytes()) - if err != nil { - return nil, err - } - m.xAddr, err = address.Format("X", hrp, m.privKey.PublicKey().Address().Bytes()) - if err != nil { - return nil, err - } - + return m, nil } // LoadSoft loads the private key from disk and creates the corresponding SoftKey. -func LoadSoft(networkID uint32, keyPath string) (*SoftKey, error) { +func LoadSoft(keyPath string) (*SoftKey, error) { kb, err := os.ReadFile(keyPath) if err != nil { return nil, err } - return LoadSoftFromBytes(networkID, kb) + return LoadSoftFromBytes(kb) +} + +func LoadSoftOrCreate(keyPath string) (*SoftKey, error) { + if utils.FileExists(keyPath) { + return LoadSoft(keyPath) + } else { + k, err := NewSoft() + if err != nil { + return nil, err + } + if err := k.Save(keyPath); err != nil { + return nil, err + } + return k, nil + } } -func LoadEwoq(networkID uint32) (*SoftKey, error) { - return LoadSoftFromBytes(networkID, ewoqKeyBytes) +func LoadEwoq() (*SoftKey, error) { + return LoadSoftFromBytes(ewoqKeyBytes) } // LoadSoftFromBytes loads the private key from bytes and creates the corresponding SoftKey. -func LoadSoftFromBytes(networkID uint32, kb []byte) (*SoftKey, error) { +func LoadSoftFromBytes(kb []byte) (*SoftKey, error) { // in case, it's already encoded - k, err := NewSoft(networkID, WithPrivateKeyEncoded(string(kb))) + k, err := NewSoft(WithPrivateKeyEncoded(string(kb))) if err == nil { return k, nil } @@ -188,7 +191,7 @@ func LoadSoftFromBytes(networkID uint32, kb []byte) (*SoftKey, error) { return nil, err } - return NewSoft(networkID, WithPrivateKey(privKey)) + return NewSoft(WithPrivateKey(privKey)) } // readASCII reads into 'buf', stopping when the buffer is full or @@ -280,12 +283,17 @@ func (m *SoftKey) PrivKeyHex() string { return hex.EncodeToString(m.privKeyRaw) } -func (m *SoftKey) P() []string { - return []string{m.pAddr} +// Saves the private key to disk with hex encoding. +func (m *SoftKey) Save(p string) error { + return os.WriteFile(p, []byte(m.PrivKeyHex()), utils.WriteReadUserOnlyPerms) +} + +func (m *SoftKey) P(networkHRP string) (string, error) { + return address.Format("P", networkHRP, m.privKey.PublicKey().Address().Bytes()) } -func (m *SoftKey) X() []string { - return []string{m.xAddr} +func (m *SoftKey) X(networkHRP string) (string, error) { + return address.Format("X", networkHRP, m.privKey.PublicKey().Address().Bytes()) } func (m *SoftKey) Spends(outputs []*avax.UTXO, opts ...OpOption) ( diff --git a/keychain/keychain.go b/keychain/keychain.go new file mode 100644 index 0000000..3cd761c --- /dev/null +++ b/keychain/keychain.go @@ -0,0 +1,125 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package keychain + +import ( + "fmt" + + "avalanche-tooling-sdk-go/avalanche" + "avalanche-tooling-sdk-go/key" + "avalanche-tooling-sdk-go/ledger" + "avalanche-tooling-sdk-go/utils" + + "github.com/ava-labs/avalanchego/utils/crypto/keychain" + + "golang.org/x/exp/maps" +) + +type Keychain struct { + keychain.Keychain + network avalanche.Network + ledgerDevice *ledger.LedgerDevice + ledgerIndices []uint32 +} + +func (kc *Keychain) P() ([]string, error) { + return utils.P(kc.network.HRP(), kc.Addresses().List()) +} + +func (kc *Keychain) LedgerEnabled() bool { + return kc.ledgerDevice != nil +} + +func (kc *Keychain) AddLedgerIndices(indices []uint32) error { + if kc.LedgerEnabled() { + kc.ledgerIndices = utils.Unique(append(kc.ledgerIndices, indices...)) + utils.Uint32Sort(kc.ledgerIndices) + newKc, err := keychain.NewLedgerKeychainFromIndices(kc.ledgerDevice, kc.ledgerIndices) + if err != nil { + return err + } + kc.Keychain = newKc + return nil + } + return fmt.Errorf("keychain is not ledger enabled") +} + +func (kc *Keychain) AddLedgerAddresses(addresses []string) error { + if kc.LedgerEnabled() { + indices, err := kc.ledgerDevice.FindAddresses(addresses, 0) + if err != nil { + return err + } + return kc.AddLedgerIndices(maps.Values(indices)) + } + return fmt.Errorf("keychain is not ledger enabled") +} + +func (kc *Keychain) AddLedgerFunds(amount uint64) error { + if kc.LedgerEnabled() { + indices, err := kc.ledgerDevice.FindFunds(kc.network, amount, 0) + if err != nil { + return err + } + return kc.AddLedgerIndices(indices) + } + return fmt.Errorf("keychain is not ledger enabled") +} + +func NewKeychain( + network avalanche.Network, + keyPath string, + useEwoq bool, + useLedger bool, + ledgerAddresses []string, + requiredFunds uint64, +) (*Keychain, error) { + // get keychain accessor + if useLedger { + dev, err := ledger.New() + if err != nil { + return nil, err + } + kc := Keychain{ + ledgerDevice: dev, + network: network, + } + if err := kc.AddLedgerIndices([]uint32{0}); err != nil { + return nil, err + } + if requiredFunds > 0 { + if err := kc.AddLedgerFunds(requiredFunds); err != nil { + return nil, err + } + } + if len(ledgerAddresses) > 0 { + if err := kc.AddLedgerAddresses(ledgerAddresses); err != nil { + return nil, err + } + } + return &kc, nil + } + if useEwoq { + sf, err := key.LoadEwoq() + if err != nil { + return nil, err + } + kc := Keychain{ + Keychain: sf.KeyChain(), + network: network, + } + return &kc, nil + } + if keyPath != "" { + sf, err := key.LoadSoft(keyPath) + if err != nil { + return nil, err + } + kc := Keychain{ + Keychain: sf.KeyChain(), + network: network, + } + return &kc, nil + } + return nil, fmt.Errorf("not keychain option defined") +} diff --git a/ledger/ledger.go b/ledger/ledger.go new file mode 100644 index 0000000..1502c9f --- /dev/null +++ b/ledger/ledger.go @@ -0,0 +1,108 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package ledger + +import ( + "avalanche-tooling-sdk-go/avalanche" + "avalanche-tooling-sdk-go/utils" + "fmt" + + "github.com/ava-labs/avalanchego/utils/crypto/keychain" + "github.com/ava-labs/avalanchego/utils/crypto/ledger" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/platformvm" +) + +const ( + maxIndexToSearch = 1000 + maxIndexToSearchForBalance = 100 +) + +type LedgerDevice struct { + keychain.Ledger +} + +func New() (*LedgerDevice, error) { + avagoDev, err := ledger.New() + if err != nil { + return nil, err + } + dev := LedgerDevice{ + Ledger: avagoDev, + } + return &dev, nil +} + +func (dev *LedgerDevice) P(network avalanche.Network, indices []uint32) ([]string, error) { + addresses, err := dev.Addresses(indices) + if err != nil { + return nil, err + } + return utils.P(network.HRP(), addresses) +} + +func (dev *LedgerDevice) FindAddresses(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) + } + // 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[string]uint32{} + for index := uint32(0); index < maxIndex; index++ { + ledgerAddress, err := dev.Addresses([]uint32{index}) + if err != nil { + return nil, err + } + for addressIndex, addr := range addressesIDs { + 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, + amount uint64, + maxIndex uint32, +) ([]uint32, error) { + pClient := platformvm.NewClient(network.Endpoint) + totalBalance := uint64(0) + indices := []uint32{} + if maxIndex == 0 { + maxIndex = maxIndexToSearchForBalance + } + for index := uint32(0); index < maxIndex; index++ { + ledgerAddress, err := dev.Addresses([]uint32{index}) + if err != nil { + return []uint32{}, err + } + ctx, cancel := utils.GetAPIContext() + resp, err := pClient.GetBalance(ctx, ledgerAddress) + cancel() + if err != nil { + return nil, err + } + if resp.Balance > 0 { + totalBalance += uint64(resp.Balance) + indices = append(indices, index) + } + if totalBalance >= amount { + break + } + } + if totalBalance < amount { + return nil, fmt.Errorf("not enough funds on ledger") + } + return indices, nil +} diff --git a/multisig/multisig.go b/multisig/multisig.go index c7a9467..259d85b 100644 --- a/multisig/multisig.go +++ b/multisig/multisig.go @@ -4,79 +4,85 @@ package multisig import ( "avalanche-tooling-sdk-go/avalanche" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/wallet/subnet/primary" ) -type PChainTxKind int - -const ( - Invalid = iota - CreateBlockchain - TransferSubnetOwnership -) +type TxKind struct { + _ string // vm + _ string // tx +} -type PChainMultisig struct { - _ *txs.Tx +type Multisig struct { + _ *txs.Tx // pChainTx } -func New(_ *txs.Tx) *PChainMultisig { +func New(_ *txs.Tx) *Multisig { return nil } -func (*PChainMultisig) ToBytes() ([]byte, error) { +func (*Multisig) String() string { + return "" +} + +func (*Multisig) ToBytes() ([]byte, error) { return nil, nil } -func (*PChainMultisig) FromBytes(_ []byte) error { +func (*Multisig) FromBytes(_ []byte) error { return nil } -func (*PChainMultisig) ToFile(_ string) error { +func (*Multisig) ToFile(_ string) error { return nil } -func (*PChainMultisig) FromFile(_ string) error { +func (*Multisig) FromFile(_ string) error { return nil } -func (*PChainMultisig) Sign(_ *primary.Wallet) error { +func (*Multisig) Sign(_ *primary.Wallet) error { return nil } -func (*PChainMultisig) Commit() error { +func (*Multisig) Commit() error { return nil } -func (*PChainMultisig) IsReadyToCommit() error { +func (*Multisig) IsReadyToCommit() error { return nil } -func (*PChainMultisig) GetRemainingSigners() ([]ids.ID, error) { +func (*Multisig) GetRemainingSigners() ([]ids.ID, error) { return nil, nil } -func (*PChainMultisig) GetAuthSigners() ([]ids.ID, error) { +func (*Multisig) GetAuthSigners() ([]ids.ID, error) { return nil, nil } -func (*PChainMultisig) GetFeeSigners() ([]ids.ID, error) { +func (*Multisig) GetFeeSigners() ([]ids.ID, error) { return nil, nil } -func (*PChainMultisig) GetKind() PChainTxKind { - return Invalid +func (*Multisig) GetTxKind() TxKind { + return TxKind{} +} + +func (*Multisig) GetNetwork() (avalanche.Network, error) { + return avalanche.Network{}, nil } -func (*PChainMultisig) GetNetwork() (avalanche.Network, error) { - return avalanche.UndefinedNetwork, nil +func (*Multisig) GetBlockchainID() (ids.ID, error) { + return ids.Empty, nil } -func (*PChainMultisig) GetSubnetID() (ids.ID, error) { +func (*Multisig) GetSubnetID() (ids.ID, error) { return ids.Empty, nil } -func (*PChainMultisig) GetSubnetOwners() ([]ids.ID, int, error) { +func (*Multisig) GetSubnetOwners() ([]ids.ID, int, error) { return nil, 0, nil } diff --git a/node/cloud.go b/node/cloud.go new file mode 100644 index 0000000..b214cfe --- /dev/null +++ b/node/cloud.go @@ -0,0 +1,56 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package node + +type CloudParams struct { + CommonParams + AWSParams + GCPParams +} + +type CommonParams struct { + // Region to use for the node + Region string + + // Image to use for the node + Image string + + // Instance type to use for the node + InstanceType string + + // Static IP to use for the node + StaticIP string +} + +// AWS Paramsific configuration +type AWSParams struct { + // AWS profile to use for the node + Profile string + + // AWS volume size in GB + VolumeSize int + + // AWS volume type + VolumeType string + + // AWS volume IOPS + VolumeIOPS int + + // AWS volume throughput + VolumeThroughput int + + // AWS security group to use for the node + SecurityGroup string +} + +type GCPParams struct { + // GCP project to use for the node + Project string + + // GCP credentials to use for the node + Credentials string + + // GCP network label to use for the node + Network string +} diff --git a/node/create.go b/node/create.go new file mode 100644 index 0000000..388bbda --- /dev/null +++ b/node/create.go @@ -0,0 +1,10 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package node + +// Create creates a new node. +// If wait is true, this function will block until the node is ready. +func Create(CloudParams CloudParams, waitForSSH bool) (Node, error) { + return Node{}, nil +} diff --git a/node/destroy.go b/node/destroy.go new file mode 100644 index 0000000..f356746 --- /dev/null +++ b/node/destroy.go @@ -0,0 +1,9 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package node + +// Destroy destroys a node. +func Destroy(node Node) error { + return nil +} diff --git a/node/exec.go b/node/exec.go new file mode 100644 index 0000000..0dea0d8 --- /dev/null +++ b/node/exec.go @@ -0,0 +1,9 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package node + +// Exec executes a command on a node. +func Exec(node Node, cmd string) error { + return nil +} diff --git a/node/net.go b/node/net.go new file mode 100644 index 0000000..a6f188f --- /dev/null +++ b/node/net.go @@ -0,0 +1,16 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package node + +import "net" + +// Connect returns the connection to the node. +func Connect(node Node) (*net.Conn, error) { + return nil, nil +} + +// Post sends a POST request to the node at the specified path with the provided body. +func Post(node Node, path string, body string) error { + return nil +} diff --git a/node/node.go b/node/node.go new file mode 100644 index 0000000..74b700c --- /dev/null +++ b/node/node.go @@ -0,0 +1,39 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package node + +// SSHConfig contains the configuration for connecting to a node over SSH +type SSHConfig struct { + // Username to use when connecting to the node + user string + + // Path to the private key to use when connecting to the node + // If this is empty, the SSH agent will be used + KeyPath string + + // Parameters to pass to the ssh command. + // See man ssh_config(5) for more information + // By defalult it's StrictHostKeyChecking=no + Params map[string]string // additional parameters to pass to the ssh command +} + +type Node struct { + // ID of the node + ID string + + // IP address of the node + IP string + + // SSH configuration for the node + SSHConfig SSHConfig + + // Cloud configuration for the node + Cloud SupportedCloud + + // CloudConfig is the cloud specific configuration for the node + CloudConfig interface{} + + // Roles of the node + Roles []SupportedRole +} diff --git a/node/supported.go b/node/supported.go new file mode 100644 index 0000000..6bdf736 --- /dev/null +++ b/node/supported.go @@ -0,0 +1,22 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package node + +type SupportedCloud int + +const ( + AWSCloud SupportedCloud = iota + GCPCloud + Docker // fake Cloud used for E2E tests +) + +type SupportedRole int + +const ( + Validator SupportedRole = iota + API + AWMRelayer +) + +// LoadTest and Monitor nodes are not supported yet diff --git a/subnet/add_validator_subnet.go b/subnet/add_validator_subnet.go index de96c71..9ec953b 100644 --- a/subnet/add_validator_subnet.go +++ b/subnet/add_validator_subnet.go @@ -4,6 +4,6 @@ package subnet // AddValidator adds validator to subnet -func AddValidator(subnet Subnet) error { +func AddValidator(_ Subnet) error { return nil } diff --git a/subnet/deploy_subnet.go b/subnet/deploy_subnet.go index e6bb037..dd71a41 100644 --- a/subnet/deploy_subnet.go +++ b/subnet/deploy_subnet.go @@ -7,20 +7,17 @@ import ( "context" "fmt" - "avalanche-tooling-sdk-go/avalanche" + "avalanche-tooling-sdk-go/multisig" + "avalanche-tooling-sdk-go/wallet" "github.com/ava-labs/avalanchego/ids" - "github.com/ava-labs/avalanchego/utils/crypto/keychain" "github.com/ava-labs/avalanchego/utils/formatting/address" - "github.com/ava-labs/avalanchego/utils/set" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/secp256k1fx" - "github.com/ava-labs/avalanchego/wallet/subnet/primary" - "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" ) // CreateSubnetTx creates uncommitted createSubnet transaction -func (c *Subnet) CreateSubnetTx(wallet primary.Wallet) (*txs.Tx, error) { +func (c *Subnet) CreateSubnetTx(wallet wallet.Wallet) (*multisig.Multisig, error) { if c.DeployInfo.ControlKeys == nil { return nil, fmt.Errorf("control keys are not provided") } @@ -49,12 +46,12 @@ func (c *Subnet) CreateSubnetTx(wallet primary.Wallet) (*txs.Tx, error) { if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil { return nil, fmt.Errorf("error signing tx: %w", err) } - return &tx, nil + return multisig.New(&tx), nil } // CreateBlockchainTx creates uncommitted createBlockchain transaction -func (c *Subnet) CreateBlockchainTx(wallet primary.Wallet, keyChain avalanche.Keychain) (*txs.Tx, error) { - if c.SubnetID == ids.Empty { +func (c *Subnet) CreateBlockchainTx(wallet wallet.Wallet) (*multisig.Multisig, error) { + if c.SubnetID == ids.Empty { return nil, fmt.Errorf("subnet ID is not provided") } if c.DeployInfo.SubnetAuthKeys == nil { @@ -69,42 +66,23 @@ func (c *Subnet) CreateBlockchainTx(wallet primary.Wallet, keyChain avalanche.Ke if c.Name == "" { return nil, fmt.Errorf("subnet name is not provided") } - fxIDs := make([]ids.ID, 0) - options := getMultisigTxOptions(keyChain.Keychain, c.DeployInfo.SubnetAuthKeys) + wallet.SetSubnetAuthMultisig(c.SubnetAuthKeys) + // create tx + fxIDs := make([]ids.ID, 0) unsignedTx, err := wallet.P().Builder().NewCreateChainTx( c.SubnetID, c.Genesis, c.VMID, fxIDs, c.Name, - options..., ) if err != nil { return nil, fmt.Errorf("error building tx: %w", err) } tx := txs.Tx{Unsigned: unsignedTx} - // sign with current wallet if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil { return nil, fmt.Errorf("error signing tx: %w", err) } - return &tx, nil -} - -func getMultisigTxOptions(keychain keychain.Keychain, subnetAuthKeys []ids.ShortID) []common.Option { - options := []common.Option{} - walletAddrs := keychain.Addresses().List() - changeAddr := walletAddrs[0] - // addrs to use for signing - customAddrsSet := set.Set[ids.ShortID]{} - customAddrsSet.Add(walletAddrs...) - customAddrsSet.Add(subnetAuthKeys...) - options = append(options, common.WithCustomAddresses(customAddrsSet)) - // set change to go to wallet addr (instead of any other subnet auth key) - changeOwner := &secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{changeAddr}, - } - options = append(options, common.WithChangeOwner(changeOwner)) - return options + return multisig.New(&tx), nil } diff --git a/subnet/join_subnet.go b/subnet/join_subnet.go index 1dbb839..e493d6b 100644 --- a/subnet/join_subnet.go +++ b/subnet/join_subnet.go @@ -4,6 +4,6 @@ package subnet // JoinSubnet configures validator node to begin validating a new Subnet -func JoinSubnet(subnet Subnet) error { +func JoinSubnet(_ Subnet) error { return nil } diff --git a/subnet/subnet.go b/subnet/subnet.go index 3ad68da..9c3a76f 100644 --- a/subnet/subnet.go +++ b/subnet/subnet.go @@ -265,6 +265,5 @@ func addTeleporterAddressToAllocations( ) core.GenesisAlloc { if alloc != nil { addAllocation(alloc, teleporterKeyAddress, teleporterKeyBalance) - } return alloc } diff --git a/subnet/subnet_test.go b/subnet/subnet_test.go index f28e29a..839cea4 100644 --- a/subnet/subnet_test.go +++ b/subnet/subnet_test.go @@ -4,6 +4,8 @@ package subnet import ( + "avalanche-tooling-sdk-go/avalanche" + "avalanche-tooling-sdk-go/wallet" "context" "fmt" "testing" @@ -14,7 +16,7 @@ import ( "github.com/ava-labs/avalanchego/wallet/subnet/primary" ) -func TestSubnetDeploy(t *testing.T) { +func TestSubnetDeploy(_ *testing.T) { // Initialize a new Avalanche Object which will be used to set shared properties // like logging, metrics preferences, etc baseApp := avalanche.New(avalanche.DefaultLeveledLogger) @@ -29,7 +31,7 @@ func TestSubnetDeploy(t *testing.T) { } newSubnet, _ := New(baseApp, &subnetParams) ctx := context.Background() - wallet, _ := primary.MakeWallet( + wallet, _ := wallet.New( ctx, &primary.WalletConfig{ URI: "", @@ -38,7 +40,7 @@ func TestSubnetDeploy(t *testing.T) { PChainTxsToFetch: nil, }, ) - // deploy Subnet returns tx ID and error + // deploy Subnet returns multisig and error deploySubnetTx, _ := newSubnet.CreateSubnetTx(wallet) fmt.Printf("deploySubnetTx %s", deploySubnetTx) } diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..6527c4f --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,73 @@ +// Copyright (C) 2022, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package utils + +import ( + "context" + "os" + "sort" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/formatting/address" +) + +const ( + APIRequestTimeout = 30 * time.Second + WriteReadUserOnlyPerms = 0o600 +) + +func MapE[T, U any](input []T, f func(T) (U, error)) ([]U, error) { + output := make([]U, 0, len(input)) + for _, e := range input { + o, err := f(e) + if err != nil { + return nil, err + } + output = append(output, o) + } + return output, nil +} + +// Unique returns a new slice containing only the unique elements from the input slice. +func Unique[T comparable](arr []T) []T { + visited := map[T]bool{} + unique := []T{} + for _, e := range arr { + if !visited[e] { + unique = append(unique, e) + visited[e] = true + } + } + return unique +} + +func Uint32Sort(arr []uint32) { + sort.Slice(arr, func(i, j int) bool { return arr[i] < arr[j] }) +} + +// Context for API requests +func GetAPIContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), APIRequestTimeout) +} + +func P( + networkHRP string, + addresses []ids.ShortID, +) ([]string, error) { + return MapE( + addresses, + func(addr ids.ShortID) (string, error) { + return address.Format("P", networkHRP, addr[:]) + }, + ) +} + +// FileExists checks if a file exists. +func FileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} diff --git a/wallet/wallet.go b/wallet/wallet.go new file mode 100644 index 0000000..8b8b70e --- /dev/null +++ b/wallet/wallet.go @@ -0,0 +1,62 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package wallet + +import ( + "avalanche-tooling-sdk-go/keychain" + "context" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/subnet/primary" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" +) + +type Wallet struct { + primary.Wallet + keychain keychain.Keychain + options []common.Option +} + +func New(ctx context.Context, config *primary.WalletConfig) (Wallet, error) { + wallet, err := primary.MakeWallet( + ctx, + config, + ) + return Wallet{ + Wallet: wallet, + keychain: keychain.Keychain{ + Keychain: config.AVAXKeychain, + }, + }, err +} + +// secure that a fee paying address (wallet's keychain) will receive the change, +// and not a randomly selected auth key that may not be paying fees +func (w *Wallet) SecureWalletIsChangeOwner() { + addrs := w.keychain.Addresses().List() + changeAddr := addrs[0] + // set change to go to wallet addr (instead of any other subnet auth key) + changeOwner := &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{changeAddr}, + } + w.options = append(w.options, common.WithChangeOwner(changeOwner)) + w.Wallet = primary.NewWalletWithOptions(w.Wallet, w.options...) +} + +// set auth keys that will also used when signing txs, besides the wallet's keychain fee paying ones +func (w *Wallet) SetAuthKeys(authKeys []ids.ShortID) { + addrs := w.keychain.Addresses().List() + addrsSet := set.Set[ids.ShortID]{} + addrsSet.Add(addrs...) + addrsSet.Add(authKeys...) + w.options = append(w.options, common.WithCustomAddresses(addrsSet)) + w.Wallet = primary.NewWalletWithOptions(w.Wallet, w.options...) +} + +func (w *Wallet) SetSubnetAuthMultisig(authKeys []ids.ShortID) { + w.SecureWalletIsChangeOwner() + w.SetAuthKeys(authKeys) +}