diff --git a/Makefile b/Makefile index 5151773..672aff0 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ mocks: mockery --disable-version-string --name AssetsAPI --keeptree --dir gen/client --output pkg/mocks mockery --disable-version-string --name StakeAPI --keeptree --dir gen/client --output pkg/mocks mockery --disable-version-string --name ValidatorsAPI --keeptree --dir gen/client --output pkg/mocks + mockery --disable-version-string --name Signable --keeptree --dir pkg/coinbase --output pkg/mocks .PHONY: docs docs: diff --git a/examples/ethereum/build-staking-operation/main.go b/examples/ethereum/build-staking-operation/main.go deleted file mode 100644 index de939c3..0000000 --- a/examples/ethereum/build-staking-operation/main.go +++ /dev/null @@ -1,94 +0,0 @@ -package main - -import ( - "context" - "log" - "math/big" - "os" - "time" - - api "github.com/coinbase/coinbase-sdk-go/gen/client" - "github.com/coinbase/coinbase-sdk-go/pkg/coinbase" -) - -/* - * This example code stakes ETH on the Holesky network. - * Run the code with 'go run examples/ethereum/build-staking-operation/main.go ' - */ - -func main() { - ctx := context.Background() - - client, err := coinbase.NewClient( - coinbase.WithAPIKeyFromJSON(os.Args[1]), - ) - if err != nil { - log.Fatalf("error creating coinbase client: %v", err) - } - - address := coinbase.NewExternalAddress("ethereum-holesky", os.Args[2]) - - stakeableBalance, err := client.GetStakeableBalance(ctx, coinbase.Eth, address, coinbase.WithStakingBalanceMode(coinbase.StakingOperationModePartial)) - if err != nil { - log.Fatalf("error getting stakeable balance: %v", err) - } - - log.Printf("stakeable balance: %s\n", stakeableBalance) - - op, err := client.BuildStakeOperation( - ctx, - big.NewFloat(0.0001), - coinbase.Eth, - address, - coinbase.WithStakingOperationMode(coinbase.StakingOperationModePartial), - ) - if err != nil { - log.Fatalf("error building staking operation: %v", err) - } - - log.Printf("staking operation ID: %s\n", op.ID()) - - for _, transaction := range op.Transactions() { - log.Printf("staking operation Transaction: %+v\n", transaction) - } - - address = coinbase.NewExternalAddress( - "ethereum-mainnet", - "0xddb00798137e9e7cc89f1e9679e6ce6ea580b8f9", - ) - - rewards, err := client.ListStakingRewards( - ctx, - coinbase.Eth, - []coinbase.Address{*address}, - time.Now().Add(-7*24*time.Hour), - time.Now(), - api.STAKINGREWARDFORMAT_USD, - ) - if err != nil { - log.Fatalf("error listing rewards: %v", err) - } - - for _, reward := range rewards { - println(reward.ToJSON()) - } - - address = coinbase.NewExternalAddress( - "ethereum-mainnet", - "0xadbf3776d60b3f9dd30cb3257b50583898745deb40cb6cb842120753bf055f6c3863e0f5bdb5c403d9aa5a275ce165e8", - ) - balances, err := client.ListHistoricalStakingBalances( - ctx, - coinbase.Eth, - address, - time.Now().Add(-7*24*time.Hour), - time.Now(), - ) - if err != nil { - log.Fatalf("error listing balances: %v", err) - } - - for _, balance := range balances { - println(balance.String()) - } -} diff --git a/examples/ethereum/list-staking-balances/main.go b/examples/ethereum/list-staking-balances/main.go new file mode 100644 index 0000000..45e7f9f --- /dev/null +++ b/examples/ethereum/list-staking-balances/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "log" + "os" + "time" + + "github.com/coinbase/coinbase-sdk-go/pkg/coinbase" +) + +/* + * This example code list historical staking balances for any ETH validator or Shared ETH Staking wallet address. + * For example: the addresses: + * * Shared ETH Staking wallet address: 0xddb00798137e9e7cc89f1e9679e6ce6ea580b8f9 + * * ETH Validator: 0xadbf3776d60b3f9dd30cb3257b50583898745deb40cb6cb842120753bf055f6c3863e0f5bdb5c403d9aa5a275ce165e8 + * Run the code with 'go run examples/ethereum/list-staking-balances/main.go ' + */ + +func main() { + ctx := context.Background() + + client, err := coinbase.NewClient( + coinbase.WithAPIKeyFromJSON(os.Args[1]), + ) + if err != nil { + log.Fatalf("error creating coinbase client: %v", err) + } + + address := coinbase.NewExternalAddress( + "ethereum-mainnet", + os.Args[1], + ) + + balances, err := client.ListHistoricalStakingBalances( + ctx, + coinbase.Eth, + address, + time.Now().Add(-7*24*time.Hour), + time.Now(), + ) + if err != nil { + log.Fatalf("error listing balances: %v", err) + } + + for _, balance := range balances { + println(balance) + } +} diff --git a/examples/ethereum/list-staking-rewards/main.go b/examples/ethereum/list-staking-rewards/main.go new file mode 100644 index 0000000..cef02f8 --- /dev/null +++ b/examples/ethereum/list-staking-rewards/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "log" + "os" + "time" + + api "github.com/coinbase/coinbase-sdk-go/gen/client" + "github.com/coinbase/coinbase-sdk-go/pkg/coinbase" +) + +/* + * This example code list historical staking rewards for any ETH validator or Shared ETH Staking wallet address. + * For example: the addresses: + * * Shared ETH Staking wallet address: 0xddb00798137e9e7cc89f1e9679e6ce6ea580b8f9 + * * ETH Validator: 0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c + * Run the code with 'go run examples/ethereum/list-staking-rewards/main.go ' + */ + +func main() { + ctx := context.Background() + + client, err := coinbase.NewClient( + coinbase.WithAPIKeyFromJSON(os.Args[1]), + ) + if err != nil { + log.Fatalf("error creating coinbase client: %v", err) + } + + address := coinbase.NewExternalAddress( + "ethereum-mainnet", + os.Args[1], + ) + + rewards, err := client.ListStakingRewards( + ctx, + coinbase.Eth, + []coinbase.Address{*address}, + time.Now().Add(-7*24*time.Hour), + time.Now(), + api.STAKINGREWARDFORMAT_USD, + ) + if err != nil { + log.Fatalf("error listing rewards: %v", err) + } + + for _, reward := range rewards { + println(reward.ToJSON()) + } +} diff --git a/examples/ethereum/stake/main.go b/examples/ethereum/stake/main.go new file mode 100644 index 0000000..997c827 --- /dev/null +++ b/examples/ethereum/stake/main.go @@ -0,0 +1,83 @@ +package main + +import ( + "context" + "log" + "math/big" + "os" + + "github.com/coinbase/coinbase-sdk-go/pkg/coinbase" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" +) + +/* + * This example code stakes ETH on the Holesky network via Shared ETH Staking. + * Run the code with 'go run examples/ethereum/stake/main.go ' + */ + +func main() { + ctx := context.Background() + + client, err := coinbase.NewClient( + coinbase.WithAPIKeyFromJSON(os.Args[1]), + ) + if err != nil { + log.Fatalf("error creating coinbase client: %v", err) + } + + address := coinbase.NewExternalAddress("ethereum-holesky", os.Args[2]) + + stakeableBalance, err := client.GetStakeableBalance(ctx, coinbase.Eth, address, coinbase.WithStakingBalanceMode(coinbase.StakingOperationModePartial)) + if err != nil { + log.Fatalf("error getting stakeable balance: %v", err) + } + + log.Printf("stakeable balance: %s\n", stakeableBalance) + + stakeOperation, err := client.BuildStakeOperation( + ctx, + big.NewFloat(0.0001), + coinbase.Eth, + address, + coinbase.WithStakingOperationMode(coinbase.StakingOperationModePartial), + ) + if err != nil { + log.Fatalf("error building staking operation: %v", err) + } + + log.Printf("staking operation ID: %s\n", stakeOperation.ID()) + + key, err := crypto.HexToECDSA(os.Args[3]) + if err != nil { + log.Fatal(err) + } + + // Sign the transactions within staking operation resource with your private key. + err = stakeOperation.Sign(key) + if err != nil { + log.Fatal(err) + } + + // For Holesky, publicly available RPC URL's can be found here https://chainlist.org/chain/17000 + ethClient, err := ethclient.Dial("") + if err != nil { + log.Fatal(err) + } + + // Broadcast each of the signed transactions to the network. + for _, transaction := range stakeOperation.Transactions() { + rawTx := transaction.Raw() + ethTx, ok := rawTx.(*types.Transaction) + if !ok { + log.Fatal("failed to cast transaction to Ethereum transaction") + } + + if err := ethClient.SendTransaction(context.Background(), ethTx); err != nil { + log.Fatal(err) + } + + log.Printf("Broadcasted transaction hash: %s", ethTx.Hash().Hex()) + } +} diff --git a/examples/solana/build-staking-operation/main.go b/examples/solana/build-staking-operation/main.go deleted file mode 100644 index f2fcf90..0000000 --- a/examples/solana/build-staking-operation/main.go +++ /dev/null @@ -1,187 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "math/big" - "os" - "time" - - "github.com/btcsuite/btcutil/base58" - "github.com/coinbase/coinbase-sdk-go/gen/client" - "github.com/coinbase/coinbase-sdk-go/pkg/coinbase" - bin "github.com/gagliardetto/binary" - "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" -) - -var ( - networkID = client.NETWORKIDENTIFIER_SOLANA_DEVNET - amount = big.NewFloat(0.1) - rpcURL = "https://api.devnet.solana.com" -) - -/* - * This example code stakes SOL on the devnet network. - * Run the code with 'go run examples/solana/build-staking-operation/main.go ' - */ - -func main() { - ctx := context.Background() - - client, err := coinbase.NewClient( - coinbase.WithAPIKeyFromJSON(os.Args[1]), - ) - if err != nil { - log.Fatalf("error creating coinbase client: %v", err) - } - - address := coinbase.NewExternalAddress(string(networkID), os.Args[2]) - - balance, err := client.GetStakeableBalance(ctx, coinbase.Sol, address) - if err != nil { - log.Fatalf("error getting balance: %v", err) - } - - log.Printf("Stakeable balance: %s\n\n", balance.Amount().String()) - - stakingOperation, err := client.BuildStakeOperation(ctx, amount, coinbase.Sol, address) - if err != nil { - log.Fatalf("error building staking operation: %v", err) - } - - log.Printf("Staking operation ID: %s\n\n", stakingOperation.ID()) - - stakingOperation, err = client.Wait(ctx, stakingOperation, coinbase.WithWaitTimeoutSeconds(60)) - if err != nil { - log.Fatalf("error waiting for staking operation: %v", err) - } - - for _, transaction := range stakingOperation.Transactions() { - log.Printf("Tx unsigned payload: %s\n\n", transaction.UnsignedPayload()) - - signedTx, err := signSolTransaction(transaction.UnsignedPayload(), []string{os.Args[3]}) - if err != nil { - log.Fatalf("error signing transaction: %v", err) - } - - log.Printf("Signed tx: %s\n\n", signedTx) - - sig, err := broadcastSolTransaction(ctx, signedTx) - if err != nil { - log.Fatalf("error broadcasting transaction: %v", err) - } - - log.Printf("Broadcasted tx: %s\n\n", getTxLink(stakingOperation.NetworkID(), sig)) - } -} -func signSolTransaction(unsignedTx string, privateKeys []string) (string, error) { - if len(privateKeys) == 0 { - return "", fmt.Errorf("need to pass at least one private key") - } - - signers := make([]solana.PrivateKey, 0, len(privateKeys)) - - for _, privateKey := range privateKeys { - signer, err := solana.PrivateKeyFromBase58(privateKey) - if err != nil { - return "", fmt.Errorf("error getting private key: %w", err) - } - - signers = append(signers, signer) - } - - data := base58.Decode(unsignedTx) - - // parse transaction - tx, err := solana.TransactionFromDecoder(bin.NewBinDecoder(data)) - if err != nil { - return "", err - } - - // clear signatures: https://github.com/gagliardetto/solana-go/issues/186 - tx.Signatures = nil - - if _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { - for _, candidate := range signers { - if candidate.PublicKey().Equals(key) { - return &candidate - } - } - - return nil - }); err != nil { - return "", fmt.Errorf("error signing transaction: %w", err) - } - - marshaledTx, err := tx.MarshalBinary() - if err != nil { - return "", fmt.Errorf("error marshaling transaction: %w", err) - } - - base58EncodedSignedTx := base58.Encode(marshaledTx) - - return base58EncodedSignedTx, nil -} - -func broadcastSolTransaction(ctx context.Context, signedTx string) (string, error) { - var ( - sig solana.Signature - err error - ) - - cluster := rpc.Cluster{ - Name: "solana-staking-demo-rpc", - RPC: rpcURL, - } - - rpcClient := rpc.New(cluster.RPC) - - data := base58.Decode(signedTx) - - // parse transaction - tx, err := solana.TransactionFromDecoder(bin.NewBinDecoder(data)) - if err != nil { - return "", err - } - - opts := rpc.TransactionOpts{ - SkipPreflight: false, - PreflightCommitment: rpc.CommitmentFinalized, - } - - fmt.Println("Sending transaction...") - - maxRetries := 20 - - for maxRetries > 0 { - fmt.Printf("Trying again [%d] Sending transaction...\n", 21-maxRetries) - - sig, err = rpcClient.SendTransactionWithOpts(ctx, tx, opts) - if err != nil { - time.Sleep(3 * time.Second) - maxRetries-- - - continue - } - - break - } - - if err != nil { - return "", fmt.Errorf("failed to send transaction: %w", err) - } - - return sig.String(), nil -} - -func getTxLink(networkID, signature string) string { - if networkID == "solana-mainnet" { - return fmt.Sprintf("https://explorer.solana.com/tx/%s", signature) - } else if networkID == "solana-devnet" { - return fmt.Sprintf("https://explorer.solana.com/tx/%s?cluster=devnet", signature) - } - - return "" -} diff --git a/examples/solana/claim-stake/main.go b/examples/solana/claim-stake/main.go new file mode 100644 index 0000000..6227b3b --- /dev/null +++ b/examples/solana/claim-stake/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "crypto/ed25519" + "fmt" + "log" + "math/big" + "os" + "time" + + "github.com/btcsuite/btcutil/base58" + "github.com/coinbase/coinbase-sdk-go/gen/client" + "github.com/coinbase/coinbase-sdk-go/pkg/coinbase" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" +) + +var ( + networkID = client.NETWORKIDENTIFIER_SOLANA_DEVNET + amount = big.NewFloat(0.01) + rpcURL = "https://api.devnet.solana.com" +) + +/* + * This example code stakes SOL on the mainnet network. + * Run the code with 'go run examples/solana/claim-stake/main.go ' + */ + +func main() { + ctx := context.Background() + + client, err := coinbase.NewClient( + coinbase.WithAPIKeyFromJSON(os.Args[1]), + coinbase.WithTimeout(10*time.Second), + ) + if err != nil { + log.Fatalf("error creating coinbase client: %v", err) + } + + address := coinbase.NewExternalAddress(string(networkID), os.Args[2]) + + balance, err := client.GetClaimableBalance(ctx, coinbase.Sol, address) + if err != nil { + log.Fatalf("error getting balance: %v", err) + } + + log.Printf("Claimable balance: %s\n\n", balance.Amount().String()) + + stakingOperation, err := client.BuildClaimStakeOperation(ctx, amount, coinbase.Sol, address) + if err != nil { + log.Fatalf("error building claim stake operation: %v", err) + } + + log.Printf("Staking operation ID: %s\n\n", stakingOperation.ID()) + + stakingOperation, err = client.Wait(ctx, stakingOperation, coinbase.WithWaitTimeoutSeconds(60)) + if err != nil { + log.Fatalf("error waiting for staking operation: %v", err) + } + + privateKey, err := decodePrivateKey(os.Args[3]) + + err = stakingOperation.Sign(privateKey) + if err != nil { + log.Fatalf("error signing transaction: %v", err) + } + + rpcClient := rpc.New(rpcURL) + + opts := rpc.TransactionOpts{ + SkipPreflight: false, + PreflightCommitment: rpc.CommitmentFinalized, + } + + for _, transaction := range stakingOperation.Transactions() { + unsignedTx := transaction.UnsignedPayload() + signedTx := transaction.SignedPayload() + + log.Printf("Unsigned tx payload: %s\n\n", unsignedTx) + log.Printf("Signed tx payload: %s\n\n", signedTx) + + rawTx := transaction.Raw() + solanaTx, ok := rawTx.(*solana.Transaction) + if !ok { + log.Fatal("failed to cast raw transaction to solana.Transaction") + } + + sig, err := rpcClient.SendTransactionWithOpts(ctx, solanaTx, opts) + if err != nil { + log.Fatalf("failed to send transaction: %v", err) + } + + log.Printf("Broadcasted tx: %s\n\n", getTxLink(stakingOperation.NetworkID(), sig.String())) + } +} + +func decodePrivateKey(privateKeyString string) (*ed25519.PrivateKey, error) { + // Decode the base58 encoded private key + privateKeyBytes := base58.Decode(privateKeyString) + if len(privateKeyBytes) != ed25519.PrivateKeySize { + log.Fatalf("invalid private key length: expected %d bytes, got %d bytes", ed25519.PrivateKeySize, len(privateKeyBytes)) + } + + // Convert the byte slice to an ed25519 private key + privateKey := ed25519.PrivateKey(privateKeyBytes) + + return &privateKey, nil +} + +func getTxLink(networkID, signature string) string { + if networkID == "solana-mainnet" { + return fmt.Sprintf("https://explorer.solana.com/tx/%s", signature) + } else if networkID == "solana-devnet" { + return fmt.Sprintf("https://explorer.solana.com/tx/%s?cluster=devnet", signature) + } + + return "" +} diff --git a/examples/solana/stake/main.go b/examples/solana/stake/main.go new file mode 100644 index 0000000..e93f7be --- /dev/null +++ b/examples/solana/stake/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "crypto/ed25519" + "fmt" + "log" + "math/big" + "os" + "time" + + "github.com/btcsuite/btcutil/base58" + "github.com/coinbase/coinbase-sdk-go/gen/client" + "github.com/coinbase/coinbase-sdk-go/pkg/coinbase" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" +) + +var ( + networkID = client.NETWORKIDENTIFIER_SOLANA_DEVNET + amount = big.NewFloat(0.01) + rpcURL = "https://api.devnet.solana.com" +) + +/* + * This example code stakes SOL on the devnet network. + * Run the code with 'go run examples/solana/stake/main.go ' + */ + +func main() { + ctx := context.Background() + + client, err := coinbase.NewClient( + coinbase.WithAPIKeyFromJSON(os.Args[1]), + coinbase.WithTimeout(10*time.Second), + ) + if err != nil { + log.Fatalf("error creating coinbase client: %v", err) + } + + address := coinbase.NewExternalAddress(string(networkID), os.Args[2]) + + balance, err := client.GetStakeableBalance(ctx, coinbase.Sol, address) + if err != nil { + log.Fatalf("error getting balance: %v", err) + } + + log.Printf("Stakeable balance: %s\n\n", balance.Amount().String()) + + stakingOperation, err := client.BuildStakeOperation(ctx, amount, coinbase.Sol, address) + if err != nil { + log.Fatalf("error building staking operation: %v", err) + } + + log.Printf("Staking operation ID: %s\n\n", stakingOperation.ID()) + + stakingOperation, err = client.Wait(ctx, stakingOperation, coinbase.WithWaitTimeoutSeconds(60)) + if err != nil { + log.Fatalf("error waiting for staking operation: %v", err) + } + + privateKey, err := decodePrivateKey(os.Args[3]) + + err = stakingOperation.Sign(privateKey) + if err != nil { + log.Fatalf("error signing transaction: %v", err) + } + + rpcClient := rpc.New(rpcURL) + + opts := rpc.TransactionOpts{ + SkipPreflight: false, + PreflightCommitment: rpc.CommitmentFinalized, + } + + for _, transaction := range stakingOperation.Transactions() { + unsignedTx := transaction.UnsignedPayload() + signedTx := transaction.SignedPayload() + + log.Printf("Unsigned tx payload: %s\n\n", unsignedTx) + log.Printf("Signed tx payload: %s\n\n", signedTx) + + rawTx := transaction.Raw() + solanaTx, ok := rawTx.(*solana.Transaction) + if !ok { + log.Fatal("failed to cast raw transaction to solana.Transaction") + } + + sig, err := rpcClient.SendTransactionWithOpts(ctx, solanaTx, opts) + if err != nil { + log.Fatalf("failed to send transaction: %v", err) + } + + log.Printf("Broadcasted tx: %s\n\n", getTxLink(stakingOperation.NetworkID(), sig.String())) + } +} + +func decodePrivateKey(privateKeyString string) (*ed25519.PrivateKey, error) { + // Decode the base58 encoded private key + privateKeyBytes := base58.Decode(privateKeyString) + if len(privateKeyBytes) != ed25519.PrivateKeySize { + log.Fatalf("invalid private key length: expected %d bytes, got %d bytes", ed25519.PrivateKeySize, len(privateKeyBytes)) + } + + // Convert the byte slice to an ed25519 private key + privateKey := ed25519.PrivateKey(privateKeyBytes) + + return &privateKey, nil +} + +func getTxLink(networkID, signature string) string { + if networkID == "solana-mainnet" { + return fmt.Sprintf("https://explorer.solana.com/tx/%s", signature) + } else if networkID == "solana-devnet" { + return fmt.Sprintf("https://explorer.solana.com/tx/%s?cluster=devnet", signature) + } + + return "" +} diff --git a/examples/solana/unstake/main.go b/examples/solana/unstake/main.go new file mode 100644 index 0000000..6c39852 --- /dev/null +++ b/examples/solana/unstake/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "crypto/ed25519" + "fmt" + "log" + "math/big" + "os" + "time" + + "github.com/btcsuite/btcutil/base58" + "github.com/coinbase/coinbase-sdk-go/gen/client" + "github.com/coinbase/coinbase-sdk-go/pkg/coinbase" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" +) + +var ( + networkID = client.NETWORKIDENTIFIER_SOLANA_DEVNET + amount = big.NewFloat(0.01) + rpcURL = "https://api.devnet.solana.com" +) + +/* + * This example code stakes SOL on the devnet network. + * Run the code with 'go run examples/solana/unstake/main.go ' + */ + +func main() { + ctx := context.Background() + + client, err := coinbase.NewClient( + coinbase.WithAPIKeyFromJSON(os.Args[1]), + coinbase.WithTimeout(10*time.Second), + ) + if err != nil { + log.Fatalf("error creating coinbase client: %v", err) + } + + address := coinbase.NewExternalAddress(string(networkID), os.Args[2]) + + balance, err := client.GetUnstakeableBalance(ctx, coinbase.Sol, address) + if err != nil { + log.Fatalf("error getting balance: %v", err) + } + + log.Printf("Unstakeable balance: %s\n\n", balance.Amount().String()) + + stakingOperation, err := client.BuildUnstakeOperation(ctx, amount, coinbase.Sol, address) + if err != nil { + log.Fatalf("error building staking operation: %v", err) + } + + log.Printf("Staking operation ID: %s\n\n", stakingOperation.ID()) + + stakingOperation, err = client.Wait(ctx, stakingOperation, coinbase.WithWaitTimeoutSeconds(60)) + if err != nil { + log.Fatalf("error waiting for staking operation: %v", err) + } + + privateKey, err := decodePrivateKey(os.Args[3]) + + err = stakingOperation.Sign(privateKey) + if err != nil { + log.Fatalf("error signing transaction: %v", err) + } + + rpcClient := rpc.New(rpcURL) + + opts := rpc.TransactionOpts{ + SkipPreflight: false, + PreflightCommitment: rpc.CommitmentFinalized, + } + + for _, transaction := range stakingOperation.Transactions() { + unsignedTx := transaction.UnsignedPayload() + signedTx := transaction.SignedPayload() + + log.Printf("Unsigned tx payload: %s\n\n", unsignedTx) + log.Printf("Signed tx payload: %s\n\n", signedTx) + + rawTx := transaction.Raw() + solanaTx, ok := rawTx.(*solana.Transaction) + if !ok { + log.Fatal("failed to cast raw transaction to solana.Transaction") + } + + sig, err := rpcClient.SendTransactionWithOpts(ctx, solanaTx, opts) + if err != nil { + log.Fatalf("failed to send transaction: %v", err) + } + + log.Printf("Broadcasted tx: %s\n\n", getTxLink(stakingOperation.NetworkID(), sig.String())) + } +} + +func decodePrivateKey(privateKeyString string) (*ed25519.PrivateKey, error) { + // Decode the base58 encoded private key + privateKeyBytes := base58.Decode(privateKeyString) + if len(privateKeyBytes) != ed25519.PrivateKeySize { + log.Fatalf("invalid private key length: expected %d bytes, got %d bytes", ed25519.PrivateKeySize, len(privateKeyBytes)) + } + + // Convert the byte slice to an ed25519 private key + privateKey := ed25519.PrivateKey(privateKeyBytes) + + return &privateKey, nil +} + +func getTxLink(networkID, signature string) string { + if networkID == "solana-mainnet" { + return fmt.Sprintf("https://explorer.solana.com/tx/%s", signature) + } else if networkID == "solana-devnet" { + return fmt.Sprintf("https://explorer.solana.com/tx/%s?cluster=devnet", signature) + } + + return "" +} diff --git a/go.mod b/go.mod index 027686d..d88392d 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,8 @@ require ( require ( filippo.io/edwards25519 v1.0.0-rc.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/blendle/zapdriver v1.3.1 // indirect @@ -23,12 +25,15 @@ require ( github.com/consensys/gnark-crypto v0.12.1 // indirect github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect github.com/holiman/uint256 v1.3.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.0 // indirect @@ -36,21 +41,26 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/supranational/blst v0.3.11 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect go.mongodb.org/mongo-driver v1.11.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/zap v1.21.0 // indirect golang.org/x/crypto v0.26.0 // indirect + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.23.0 // indirect golang.org/x/term v0.23.0 // indirect diff --git a/go.sum b/go.sum index e7b016b..6edc6a9 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI= github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= @@ -51,6 +53,8 @@ github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/Yj github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I= github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= @@ -59,6 +63,8 @@ github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= @@ -72,20 +78,27 @@ github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0/go.mod h1:D9A github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= github.com/gagliardetto/solana-go v1.10.0 h1:lDuHGC+XLxw9j8fCHBZM9tv4trI0PVhev1m9NAMaIdM= github.com/gagliardetto/solana-go v1.10.0/go.mod h1:afBEcIRrDLJst3lvAahTr63m6W2Ns6dajZxe2irF7Jg= github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= @@ -99,9 +112,21 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -136,6 +161,10 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= @@ -173,10 +202,16 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +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/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -200,9 +235,15 @@ 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-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.mongodb.org/mongo-driver v1.11.0 h1:FZKhBSTydeuffHj9CBjXlR8vQLee1cQyTWYPA6/tqiE= @@ -248,13 +289,17 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -284,6 +329,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/pkg/auth/transport.go b/pkg/auth/transport.go index 05fd388..92d6aae 100644 --- a/pkg/auth/transport.go +++ b/pkg/auth/transport.go @@ -36,7 +36,7 @@ func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { "Correlation-Context", fmt.Sprintf( "%s,%s", - fmt.Sprintf("%s=%s", "sdk_version", "0.0.9"), + fmt.Sprintf("%s=%s", "sdk_version", "0.0.10"), fmt.Sprintf("%s=%s", "sdk_language", "go"), ), ) diff --git a/pkg/coinbase/client.go b/pkg/coinbase/client.go index c7903d1..bbdbeb2 100644 --- a/pkg/coinbase/client.go +++ b/pkg/coinbase/client.go @@ -69,27 +69,35 @@ func WithDebug() ClientOption { } } +// WithTimeout sets the timeout for the client +func WithTimeout(timeout time.Duration) ClientOption { + return func(c *Client) error { + c.baseHTTPClient.Timeout = timeout + return nil + } +} + // NewClient creates a new coinbase client with the supplied options. func NewClient(o ...ClientOption) (*Client, error) { c := &Client{ cfg: client.NewConfiguration(), } - for _, opt := range o { - if err := opt(c); err != nil { - return nil, err - } - } - if c.baseHTTPClient == nil { c.baseHTTPClient = &http.Client{ - Timeout: time.Second * 10, + Timeout: time.Second * 5, Transport: &http.Transport{ TLSHandshakeTimeout: 5 * time.Second, }, } } + for _, opt := range o { + if err := opt(c); err != nil { + return nil, err + } + } + c.cfg.HTTPClient = c.baseHTTPClient if c.cfg.HTTPClient.Transport == nil { diff --git a/pkg/coinbase/ethereum_signable_test.go b/pkg/coinbase/ethereum_signable_test.go new file mode 100644 index 0000000..a899c7a --- /dev/null +++ b/pkg/coinbase/ethereum_signable_test.go @@ -0,0 +1,100 @@ +package coinbase_test + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "math/big" + "testing" + + "github.com/coinbase/coinbase-sdk-go/pkg/coinbase" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/suite" +) + +type EthereumSignableSuite struct { + suite.Suite + privKey *ecdsa.PrivateKey + pubKey ecdsa.PublicKey + signable *coinbase.EthereumSignable +} + +func TestEthereumSignableSuite(t *testing.T) { + suite.Run(t, new(EthereumSignableSuite)) +} + +func (s *EthereumSignableSuite) SetupTest() { + // Generate a new ECDSA key pair + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + s.NoError(err) + + s.privKey = privKey + s.pubKey = privKey.PublicKey + + // Create a fake transaction (for example purposes) + tx := types.NewTransaction( + 0, // Nonce + crypto.PubkeyToAddress(s.pubKey), // To address + big.NewInt(1000000000000000000), // Value (1 ETH) + 21000, // Gas limit + big.NewInt(1), // Gas price + nil, // Data + ) + + // Create an EthereumSignable instance + s.signable = coinbase.NewEthereumSignable(tx) +} + +func (s *EthereumSignableSuite) TestSign_InvalidKeyType_RSA() { + // Generate RSA key + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + s.NoError(err) + + // Test with RSA key + _, err = s.signable.Sign(rsaKey) + s.Error(err) + s.Equal("provided key is not an *ecdsa.PrivateKey", err.Error()) +} + +func (s *EthereumSignableSuite) TestSign_InvalidKeyType_ED25519() { + // Generate ECDSA key with a different curve + _, privateKey, err := ed25519.GenerateKey(rand.Reader) + + s.NoError(err) + + // Test with ECDSA key + _, err = s.signable.Sign(privateKey) + s.Error(err) + s.Equal("provided key is not an *ecdsa.PrivateKey", err.Error()) +} + +func (s *EthereumSignableSuite) TestSign_SuccessfulSigning() { + // Call the Sign method + signedTx, err := s.signable.Sign(s.privKey) + + // Assert no error and check the signed transaction + s.NoError(err) + s.NotEmpty(signedTx) + + // Verify that the signable is signed + s.True(s.signable.IsSigned()) +} + +func (s *EthereumSignableSuite) TestSign_NewPublicKey() { + // Generate a new ECDSA key pair + newPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + s.NoError(err) + + // Call the Sign method with the new private key + signedTx, err := s.signable.Sign(newPrivKey) + + // Assert no error and check the signed transaction + s.NoError(err) + s.NotEmpty(signedTx) + + // Verify that the signable is signed + s.True(s.signable.IsSigned()) +} diff --git a/pkg/coinbase/signable.go b/pkg/coinbase/signable.go new file mode 100644 index 0000000..928a753 --- /dev/null +++ b/pkg/coinbase/signable.go @@ -0,0 +1,143 @@ +package coinbase + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "encoding/hex" + "fmt" + "math/big" + + "github.com/btcsuite/btcutil/base58" + "github.com/ethereum/go-ethereum/core/types" + "github.com/gagliardetto/solana-go" +) + +// SolanaSignable implements the Signable interface +type SolanaSignable struct { + raw *solana.Transaction +} + +func NewSolanaSignable(raw *solana.Transaction) *SolanaSignable { + return &SolanaSignable{raw: raw} +} + +func (s *SolanaSignable) Sign(k crypto.Signer) (string, error) { + if s.IsSigned() { + bytes, err := s.raw.MarshalBinary() + if err != nil { + return "", err + } + + return base58.Encode(bytes), nil + } + + ed25519Key, ok := k.(*ed25519.PrivateKey) + if !ok { + return "", fmt.Errorf("provided key is not an *ed25519.PrivateKey") + } + + solanaPrivateKey := solana.PrivateKey(*ed25519Key) + + // clear signatures: https://github.com/gagliardetto/solana-go/issues/186 + s.raw.Signatures = nil + + // Sign the transaction + if _, err := s.raw.Sign(func(key solana.PublicKey) *solana.PrivateKey { + if solanaPrivateKey.PublicKey().Equals(key) { + return &solanaPrivateKey + } + return nil + }); err != nil { + return "", fmt.Errorf("error signing transaction: %w", err) + } + + // Marshal the signed transaction to binary + marshaledTx, err := s.raw.MarshalBinary() + if err != nil { + return "", fmt.Errorf("error marshaling transaction: %w", err) + } + + // Encode the binary transaction using base58 + base58EncodedSignedTx := base58.Encode(marshaledTx) + + return base58EncodedSignedTx, nil +} + +func (s *SolanaSignable) IsSigned() bool { + if s.raw.Signatures == nil { + return false + } + + if len(s.raw.Signatures) == 0 { + return false + } + + // Check for the placeholder signature. + if len(s.raw.Signatures) == 1 && s.raw.Signatures[0].Equals(solana.Signature{}) { + return false + } + + return true +} + +func (s *SolanaSignable) Raw() interface{} { + return s.raw +} + +// EthereumSignable implements the Signable interface +type EthereumSignable struct { + raw *types.Transaction +} + +func NewEthereumSignable(raw *types.Transaction) *EthereumSignable { + return &EthereumSignable{raw: raw} +} + +func (e *EthereumSignable) Sign(k crypto.Signer) (string, error) { + if e.IsSigned() { + bytes, err := e.raw.MarshalBinary() + if err != nil { + return "", err + } + + return hex.EncodeToString(bytes), nil + } + + ecdsaKey, ok := k.(*ecdsa.PrivateKey) + if !ok { + return "", fmt.Errorf("provided key is not an *ecdsa.PrivateKey") + } + + signer := types.LatestSignerForChainID(e.raw.ChainId()) + signedTx, err := types.SignTx(e.raw, signer, ecdsaKey) + if err != nil { + return "", err + } + + bytes, err := signedTx.MarshalBinary() + if err != nil { + return "", err + } + + e.raw = signedTx + return hex.EncodeToString(bytes), nil +} + +func (e *EthereumSignable) IsSigned() bool { + v, r, s := e.raw.RawSignatureValues() + if v != nil && v.Cmp(big.NewInt(0)) != 0 { + return true + } + if r != nil && r.Cmp(big.NewInt(0)) != 0 { + return true + } + if s != nil && s.Cmp(big.NewInt(0)) != 0 { + return true + } + return false +} + +func (e *EthereumSignable) Raw() interface{} { + return e.raw +} diff --git a/pkg/coinbase/solana_signable_test.go b/pkg/coinbase/solana_signable_test.go new file mode 100644 index 0000000..68f59a8 --- /dev/null +++ b/pkg/coinbase/solana_signable_test.go @@ -0,0 +1,128 @@ +package coinbase_test + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "log" + "testing" + + "github.com/coinbase/coinbase-sdk-go/pkg/coinbase" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/system" + "github.com/stretchr/testify/suite" +) + +type SolanaSignableSuite struct { + suite.Suite + + privateKey ed25519.PrivateKey + signable *coinbase.SolanaSignable +} + +func TestSolanaSignableSuite(t *testing.T) { + suite.Run(t, new(SolanaSignableSuite)) +} + +func (s *SolanaSignableSuite) SetupTest() { + var err error + var pubKey ed25519.PublicKey + pubKey, s.privateKey, err = ed25519.GenerateKey(rand.Reader) + s.NoError(err) + + // Use a fake blockhash + fakeBlockhash := solana.MustHashFromBase58("11111111111111111111111111111111") + + // Generate a random recipient public key + recipientPubKey := solana.NewWallet().PublicKey() + + // Create a transfer instruction to send 1 SOL to the recipient + transferInstruction := system.NewTransferInstruction( + 1_000_000_000, + solana.PublicKeyFromBytes(pubKey[:]), + recipientPubKey, + ).Build() + + // Create a new transaction + tx, err := solana.NewTransaction( + []solana.Instruction{ + transferInstruction, + }, + fakeBlockhash, + ) + s.NoError(err) + + // Create a SolanaSignable instance + s.signable = coinbase.NewSolanaSignable(tx) +} + +func (s *SolanaSignableSuite) TestSign_InvalidKeyType_RSA() { + // Generate RSA key + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + s.NoError(err) + + // Test with RSA key + _, err = s.signable.Sign(rsaKey) + s.Error(err) + s.Equal("provided key is not an *ed25519.PrivateKey", err.Error()) +} + +func (s *SolanaSignableSuite) TestSign_InvalidKeyType_ECDSA() { + // Generate ECDSA key + ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + s.NoError(err) + + // Test with ECDSA key + _, err = s.signable.Sign(ecdsaKey) + s.Error(err) + s.Equal("provided key is not an *ed25519.PrivateKey", err.Error()) +} + +func (s *SolanaSignableSuite) TestSign_CorrectPublicKey() { + s.False(s.signable.IsSigned()) + + // Call the Sign method with the new private key + signedTx, err := s.signable.Sign(&s.privateKey) + + // Assert no error and check the signed transaction + s.NoError(err) + s.NotEmpty(signedTx) + + // Verify that the signable is signed + s.True(s.signable.IsSigned()) +} + +func (s *SolanaSignableSuite) TestSign_IncorrectPublicKey() { + // Generate a new ed25519 key pair + _, newPrivKey, err := ed25519.GenerateKey(rand.Reader) + s.NoError(err) + + s.False(s.signable.IsSigned()) + + // Call the Sign method with the new private key + signedTx, err := s.signable.Sign(&newPrivKey) + + // Assert no error and check the signed transaction + s.Error(err) + s.Empty(signedTx) + + // Verify that the signable is still not signed + s.False(s.signable.IsSigned()) +} + +func (s *SolanaSignableSuite) TestRaw_Parsable() { + s.False(s.signable.IsSigned()) + + // Call the Sign method with the new private key + raw := s.signable.Raw() + + s.NotEmpty(raw) + + rawTx := s.signable.Raw() + _, ok := rawTx.(*solana.Transaction) + if !ok { + log.Fatal("failed to cast raw transaction to solana.Transaction") + } +} diff --git a/pkg/coinbase/staking_operation.go b/pkg/coinbase/staking_operation.go index f936c71..3b93bc8 100644 --- a/pkg/coinbase/staking_operation.go +++ b/pkg/coinbase/staking_operation.go @@ -2,7 +2,7 @@ package coinbase import ( "context" - "crypto/ecdsa" + "crypto" "encoding/base64" "fmt" "math/big" @@ -159,10 +159,9 @@ func (s *StakingOperation) Transactions() []*Transaction { return s.transactions } -// Sign will sign each transaction using the supplied key -// This will halt and return an error if any of the transactions -// fail to sign. -func (s *StakingOperation) Sign(k *ecdsa.PrivateKey) error { +// Sign will sign each transaction using the supplied key if it isn't already signed. +// This will halt and return an error if any of the transactions fail to sign. +func (s *StakingOperation) Sign(k crypto.Signer) error { for _, tx := range s.Transactions() { if !tx.IsSigned() { err := tx.Sign(k) diff --git a/pkg/coinbase/staking_operation_test.go b/pkg/coinbase/staking_operation_test.go index 3099bfb..00b6082 100644 --- a/pkg/coinbase/staking_operation_test.go +++ b/pkg/coinbase/staking_operation_test.go @@ -2,23 +2,35 @@ package coinbase import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "errors" "fmt" "net/http" "testing" + "github.com/coinbase/coinbase-sdk-go/gen/client" api "github.com/coinbase/coinbase-sdk-go/gen/client" "github.com/coinbase/coinbase-sdk-go/pkg/mocks" "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" ) -func TestStakingOperation_Wait_Success(t *testing.T) { +type StakingOperationSuite struct { + suite.Suite +} + +func TestStakingOperationSuite(t *testing.T) { + suite.Run(t, new(StakingOperationSuite)) +} + +func (s *StakingOperationSuite) TestStakingOperation_Wait_Success() { mc := &mockController{ - stakeAPI: mocks.NewStakeAPI(t), + stakeAPI: mocks.NewStakeAPI(s.T()), } - mockGetExternalStakingOperation(t, mc.stakeAPI, http.StatusOK, "pending") + mockGetExternalStakingOperation(s.T(), mc.stakeAPI, http.StatusOK, "pending") c := &Client{ client: &api.APIClient{ @@ -27,26 +39,26 @@ func TestStakingOperation_Wait_Success(t *testing.T) { }, } - so, err := mockStakingOperation(t, "pending") - require.NoError(t, err, "failed to create staking operation") + so, err := mockStakingOperation(s.T(), "pending") + s.NoError(err, "failed to create staking operation") key, err := crypto.GenerateKey() - require.NoError(t, err, "failed to generate ecdsa key") + s.NoError(err, "failed to generate ecdsa key") err = so.Sign(key) - require.NoError(t, err, "failed to sign staking operation") + s.NoError(err, "failed to sign staking operation") signedPayload := so.Transactions()[0].SignedPayload() - require.NotEmpty(t, signedPayload, "signed payload should not be empty") + s.NotEmpty(signedPayload, "signed payload should not be empty") so, err = c.Wait(context.Background(), so) - assert.NoError(t, err, "staking operation wait should not error") - assert.Equal(t, "complete", so.Status(), "staking operation status should be complete") - assert.Equal(t, 1, len(so.Transactions()), "staking operation should have 1 transaction") - assert.Equal(t, signedPayload, so.Transactions()[0].SignedPayload(), "staking operation signed payload should not have changed") + s.NoError(err, "staking operation wait should not error") + s.Equal("complete", so.Status(), "staking operation status should be complete") + s.Equal(1, len(so.Transactions()), "staking operation should have 1 transaction") + s.Equal(signedPayload, so.Transactions()[0].SignedPayload(), "staking operation signed payload should not have changed") } -func TestStakingOperation_Wait_Success_CustomOptions(t *testing.T) { +func (s *StakingOperationSuite) TestStakingOperation_Wait_Success_CustomOptions() { mc := &mockController{ - stakeAPI: mocks.NewStakeAPI(t), + stakeAPI: mocks.NewStakeAPI(s.T()), } - mockGetExternalStakingOperation(t, mc.stakeAPI, http.StatusOK, "pending") + mockGetExternalStakingOperation(s.T(), mc.stakeAPI, http.StatusOK, "pending") c := &Client{ client: &api.APIClient{ @@ -55,20 +67,20 @@ func TestStakingOperation_Wait_Success_CustomOptions(t *testing.T) { }, } - so, err := mockStakingOperation(t, "pending") - assert.NoError(t, err, "staking operation creation should not error") + so, err := mockStakingOperation(s.T(), "pending") + s.NoError(err, "staking operation creation should not error") so, err = c.Wait( context.Background(), so, WithWaitIntervalSeconds(1), WithWaitTimeoutSeconds(3), ) - assert.NoError(t, err, "staking operation wait should not error") - assert.Equal(t, "complete", so.Status(), "staking operation status should be complete") - assert.Equal(t, 1, len(so.Transactions()), "staking operation should have 1 transaction") + s.NoError(err, "staking operation wait should not error") + s.Equal("complete", so.Status(), "staking operation status should be complete") + s.Equal(1, len(so.Transactions()), "staking operation should have 1 transaction") } -func TestStakingOperation_Wait_Failure(t *testing.T) { +func (s *StakingOperationSuite) TestStakingOperation_Wait_Failure() { tests := map[string]struct { soStatus string setup func(*mockController) @@ -90,7 +102,7 @@ func TestStakingOperation_Wait_Failure(t *testing.T) { } for name, tt := range tests { - t.Run(name, func(t *testing.T) { + s.T().Run(name, func(t *testing.T) { mc := &mockController{ stakeAPI: mocks.NewStakeAPI(t), } @@ -104,22 +116,22 @@ func TestStakingOperation_Wait_Failure(t *testing.T) { } so, err := mockStakingOperation(t, tt.soStatus) - assert.NoError(t, err, "staking operation creation should not error") + s.NoError(err, "staking operation creation should not error") _, err = c.Wait(context.Background(), so) - assert.Error(t, err, "staking operation wait should error") + s.Error(err, "staking operation wait should error") }) } } -func TestStakingOperation_GetSignedVoluntaryExitMessages(t *testing.T) { - stakingOperation, err := mockStakingOperation(t, "pending") - assert.NoError(t, err, "staking operation creation should not error") +func (s *StakingOperationSuite) TestStakingOperation_GetSignedVoluntaryExitMessages() { + stakingOperation, err := mockStakingOperation(s.T(), "pending") + s.NoError(err, "staking operation creation should not error") SignedVoluntaryExitMessages, err := stakingOperation.GetSignedVoluntaryExitMessages() - assert.NoError(t, err, "get signed voluntary exit messages should not error") + s.NoError(err, "get signed voluntary exit messages should not error") - assert.Equal(t, 1, len(SignedVoluntaryExitMessages), "signed voluntary exit messages should have length 1") - assert.Equal(t, "test-data", SignedVoluntaryExitMessages[0], "signed voluntary exit messages should match") + s.Equal(1, len(SignedVoluntaryExitMessages), "signed voluntary exit messages should have length 1") + s.Equal("test-data", SignedVoluntaryExitMessages[0], "signed voluntary exit messages should match") } func mockStakingOperation(t *testing.T, status string) (*StakingOperation, error) { @@ -214,3 +226,110 @@ func mockGetExternalStakingOperation(t *testing.T, stakeAPI *mocks.StakeAPI, sta nil, ).Once() } + +func (s *StakingOperationSuite) TestSign_AllTransactionsSigned() { + // Create mock transactions + signable1 := new(mocks.Signable) + signable2 := new(mocks.Signable) + + // Set expectations + signable1.On("IsSigned").Return(true) + signable2.On("IsSigned").Return(true) + + stakingOp := &StakingOperation{} + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + s.NoError(err) + + // Set up the staking operation to return these transactions + stakingOp.transactions = []*Transaction{ + { + model: &client.Transaction{}, + signable: signable1, + }, + { + model: &client.Transaction{}, + signable: signable2, + }, + } + + // Call the Sign method + err = stakingOp.Sign(signer) + + // Assert no error and that no transactions were signed + s.NoError(err) + signable1.AssertNotCalled(s.T(), "Sign", mock.Anything) + signable2.AssertNotCalled(s.T(), "Sign", mock.Anything) +} + +func (s *StakingOperationSuite) TestSign_SomeTransactionsNotSigned() { + // Create mock transactions + signable1 := new(mocks.Signable) + signable2 := new(mocks.Signable) + + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + s.NoError(err) + + // Set expectations + signable1.On("IsSigned").Return(true) + signable2.On("IsSigned").Return(false) + signable2.On("Sign", signer).Return("", nil) + + stakingOp := &StakingOperation{} + + // Set up the staking operation to return these transactions + stakingOp.transactions = []*Transaction{ + { + model: &client.Transaction{}, + signable: signable1, + }, + { + model: &client.Transaction{}, + signable: signable2, + }, + } + + // Call the Sign method + err = stakingOp.Sign(signer) + + // Assert no error and that the second transaction was signed + s.NoError(err) + signable1.AssertNotCalled(s.T(), "Sign", mock.Anything) + signable2.AssertCalled(s.T(), "Sign", signer) +} + +func (s *StakingOperationSuite) TestSign_SignTransactionFails() { + // Create mock transactions + signable1 := new(mocks.Signable) + signable2 := new(mocks.Signable) + + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + s.NoError(err) + + // Set expectations + signable1.On("IsSigned").Return(true) + signable2.On("IsSigned").Return(false) + signable2.On("Sign", signer).Return("", errors.New("signing failed")) + + stakingOp := &StakingOperation{} + + // Set up the staking operation to return these transactions + stakingOp.transactions = []*Transaction{ + { + model: &client.Transaction{}, + signable: signable1, + }, + { + model: &client.Transaction{}, + signable: signable2, + }, + } + + // Call the Sign method + err = stakingOp.Sign(signer) + + // Assert error and that the second transaction was not signed + s.Error(err) + s.EqualError(err, "signing failed") + signable1.AssertNotCalled(s.T(), "Sign", mock.Anything) + signable2.AssertCalled(s.T(), "Sign", signer) +} diff --git a/pkg/coinbase/transaction.go b/pkg/coinbase/transaction.go index ab3001c..93b22a9 100644 --- a/pkg/coinbase/transaction.go +++ b/pkg/coinbase/transaction.go @@ -1,20 +1,28 @@ package coinbase import ( - "crypto/ecdsa" + "crypto" "encoding/hex" "fmt" - "math/big" "strings" + "github.com/btcsuite/btcutil/base58" "github.com/coinbase/coinbase-sdk-go/gen/client" "github.com/ethereum/go-ethereum/core/types" + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" ) // Transaction represents an onchain transaction type Transaction struct { - model *client.Transaction - raw *types.Transaction + model *client.Transaction + signable Signable +} + +type Signable interface { + Sign(crypto.Signer) (string, error) + IsSigned() bool + Raw() interface{} } // UnsignedPayload returns the unsigned payload of the transaction @@ -59,24 +67,14 @@ func (t *Transaction) FromAddressID() string { return t.model.FromAddressId } -// Raw returns the raw transaction in types.Transaction format -func (t *Transaction) Raw() *types.Transaction { - return t.raw +// Raw returns the raw transaction in the underlying blockchain's format +func (t *Transaction) Raw() interface{} { + return t.signable.Raw() } // IsSigned returns true if the transaction is signed func (t *Transaction) IsSigned() bool { - v, r, s := t.Raw().RawSignatureValues() - if v != nil && v.Cmp(big.NewInt(0)) != 0 { - return true - } - if r != nil && r.Cmp(big.NewInt(0)) != 0 { - return true - } - if s != nil && s.Cmp(big.NewInt(0)) != 0 { - return true - } - return false + return t.signable.IsSigned() } // String returns a string representation of the transaction @@ -85,25 +83,18 @@ func (t *Transaction) String() string { } // Sign will sign the transaction using the supplied key -func (t *Transaction) Sign(k *ecdsa.PrivateKey) error { +func (t *Transaction) Sign(k crypto.Signer) error { if t.IsSigned() { return nil } - signer := types.LatestSignerForChainID(t.Raw().ChainId()) - signedTx, err := types.SignTx(t.Raw(), signer, k) + signedTx, err := t.signable.Sign(k) if err != nil { return err } - bytes, err := signedTx.MarshalBinary() - if err != nil { - return err - } + t.model.SignedPayload = &signedTx - signedPayload := hex.EncodeToString(bytes) - t.model.SignedPayload = &signedPayload - t.raw = signedTx return nil } @@ -126,7 +117,18 @@ func newTransactionFromModel(m *client.Transaction) (*Transaction, error) { return nil, err } - resp.raw = t + resp.signable = &EthereumSignable{raw: t} + } else if strings.HasPrefix(m.NetworkId, "solana") { + data := base58.Decode(m.UnsignedPayload) + + tx, err := solana.TransactionFromDecoder(bin.NewBinDecoder(data)) + if err != nil { + return nil, err + } + + resp.signable = &SolanaSignable{raw: tx} + } else { + return nil, fmt.Errorf("unsupported network id: %s", m.NetworkId) } resp.model = m diff --git a/pkg/mocks/Signable.go b/pkg/mocks/Signable.go new file mode 100644 index 0000000..adddbca --- /dev/null +++ b/pkg/mocks/Signable.go @@ -0,0 +1,82 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + crypto "crypto" + + mock "github.com/stretchr/testify/mock" +) + +// Signable is an autogenerated mock type for the Signable type +type Signable struct { + mock.Mock +} + +// IsSigned provides a mock function with given fields: +func (_m *Signable) IsSigned() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Raw provides a mock function with given fields: +func (_m *Signable) Raw() interface{} { + ret := _m.Called() + + var r0 interface{} + if rf, ok := ret.Get(0).(func() interface{}); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + +// Sign provides a mock function with given fields: _a0 +func (_m *Signable) Sign(_a0 crypto.Signer) (string, error) { + ret := _m.Called(_a0) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(crypto.Signer) (string, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(crypto.Signer) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(crypto.Signer) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewSignable creates a new instance of Signable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSignable(t interface { + mock.TestingT + Cleanup(func()) +}) *Signable { + mock := &Signable{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/mocks/StakeAPI.go b/pkg/mocks/StakeAPI.go index 2aeba87..56883dd 100644 --- a/pkg/mocks/StakeAPI.go +++ b/pkg/mocks/StakeAPI.go @@ -17,55 +17,6 @@ type StakeAPI struct { mock.Mock } -// BroadcastStakingOperation provides a mock function with given fields: ctx, walletId, addressId, stakingOperationId -func (_m *StakeAPI) BroadcastStakingOperation(ctx context.Context, walletId string, addressId string, stakingOperationId string) client.ApiBroadcastStakingOperationRequest { - ret := _m.Called(ctx, walletId, addressId, stakingOperationId) - - var r0 client.ApiBroadcastStakingOperationRequest - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) client.ApiBroadcastStakingOperationRequest); ok { - r0 = rf(ctx, walletId, addressId, stakingOperationId) - } else { - r0 = ret.Get(0).(client.ApiBroadcastStakingOperationRequest) - } - - return r0 -} - -// BroadcastStakingOperationExecute provides a mock function with given fields: r -func (_m *StakeAPI) BroadcastStakingOperationExecute(r client.ApiBroadcastStakingOperationRequest) (*client.StakingOperation, *http.Response, error) { - ret := _m.Called(r) - - var r0 *client.StakingOperation - var r1 *http.Response - var r2 error - if rf, ok := ret.Get(0).(func(client.ApiBroadcastStakingOperationRequest) (*client.StakingOperation, *http.Response, error)); ok { - return rf(r) - } - if rf, ok := ret.Get(0).(func(client.ApiBroadcastStakingOperationRequest) *client.StakingOperation); ok { - r0 = rf(r) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*client.StakingOperation) - } - } - - if rf, ok := ret.Get(1).(func(client.ApiBroadcastStakingOperationRequest) *http.Response); ok { - r1 = rf(r) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*http.Response) - } - } - - if rf, ok := ret.Get(2).(func(client.ApiBroadcastStakingOperationRequest) error); ok { - r2 = rf(r) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - // BuildStakingOperation provides a mock function with given fields: ctx func (_m *StakeAPI) BuildStakingOperation(ctx context.Context) client.ApiBuildStakingOperationRequest { ret := _m.Called(ctx) @@ -115,55 +66,6 @@ func (_m *StakeAPI) BuildStakingOperationExecute(r client.ApiBuildStakingOperati return r0, r1, r2 } -// CreateStakingOperation provides a mock function with given fields: ctx, walletId, addressId -func (_m *StakeAPI) CreateStakingOperation(ctx context.Context, walletId string, addressId string) client.ApiCreateStakingOperationRequest { - ret := _m.Called(ctx, walletId, addressId) - - var r0 client.ApiCreateStakingOperationRequest - if rf, ok := ret.Get(0).(func(context.Context, string, string) client.ApiCreateStakingOperationRequest); ok { - r0 = rf(ctx, walletId, addressId) - } else { - r0 = ret.Get(0).(client.ApiCreateStakingOperationRequest) - } - - return r0 -} - -// CreateStakingOperationExecute provides a mock function with given fields: r -func (_m *StakeAPI) CreateStakingOperationExecute(r client.ApiCreateStakingOperationRequest) (*client.StakingOperation, *http.Response, error) { - ret := _m.Called(r) - - var r0 *client.StakingOperation - var r1 *http.Response - var r2 error - if rf, ok := ret.Get(0).(func(client.ApiCreateStakingOperationRequest) (*client.StakingOperation, *http.Response, error)); ok { - return rf(r) - } - if rf, ok := ret.Get(0).(func(client.ApiCreateStakingOperationRequest) *client.StakingOperation); ok { - r0 = rf(r) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*client.StakingOperation) - } - } - - if rf, ok := ret.Get(1).(func(client.ApiCreateStakingOperationRequest) *http.Response); ok { - r1 = rf(r) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*http.Response) - } - } - - if rf, ok := ret.Get(2).(func(client.ApiCreateStakingOperationRequest) error); ok { - r2 = rf(r) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - // FetchHistoricalStakingBalances provides a mock function with given fields: ctx, networkId, addressId func (_m *StakeAPI) FetchHistoricalStakingBalances(ctx context.Context, networkId string, addressId string) client.ApiFetchHistoricalStakingBalancesRequest { ret := _m.Called(ctx, networkId, addressId) @@ -360,55 +262,6 @@ func (_m *StakeAPI) GetStakingContextExecute(r client.ApiGetStakingContextReques return r0, r1, r2 } -// GetStakingOperation provides a mock function with given fields: ctx, walletId, addressId, stakingOperationId -func (_m *StakeAPI) GetStakingOperation(ctx context.Context, walletId string, addressId string, stakingOperationId string) client.ApiGetStakingOperationRequest { - ret := _m.Called(ctx, walletId, addressId, stakingOperationId) - - var r0 client.ApiGetStakingOperationRequest - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) client.ApiGetStakingOperationRequest); ok { - r0 = rf(ctx, walletId, addressId, stakingOperationId) - } else { - r0 = ret.Get(0).(client.ApiGetStakingOperationRequest) - } - - return r0 -} - -// GetStakingOperationExecute provides a mock function with given fields: r -func (_m *StakeAPI) GetStakingOperationExecute(r client.ApiGetStakingOperationRequest) (*client.StakingOperation, *http.Response, error) { - ret := _m.Called(r) - - var r0 *client.StakingOperation - var r1 *http.Response - var r2 error - if rf, ok := ret.Get(0).(func(client.ApiGetStakingOperationRequest) (*client.StakingOperation, *http.Response, error)); ok { - return rf(r) - } - if rf, ok := ret.Get(0).(func(client.ApiGetStakingOperationRequest) *client.StakingOperation); ok { - r0 = rf(r) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*client.StakingOperation) - } - } - - if rf, ok := ret.Get(1).(func(client.ApiGetStakingOperationRequest) *http.Response); ok { - r1 = rf(r) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*http.Response) - } - } - - if rf, ok := ret.Get(2).(func(client.ApiGetStakingOperationRequest) error); ok { - r2 = rf(r) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - // NewStakeAPI creates a new instance of StakeAPI. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewStakeAPI(t interface { diff --git a/pkg/mocks/ValidatorsAPI.go b/pkg/mocks/ValidatorsAPI.go index c4fa715..a6c408c 100644 --- a/pkg/mocks/ValidatorsAPI.go +++ b/pkg/mocks/ValidatorsAPI.go @@ -21,10 +21,6 @@ type ValidatorsAPI struct { func (_m *ValidatorsAPI) GetValidator(ctx context.Context, networkId string, assetId string, validatorId string) client.ApiGetValidatorRequest { ret := _m.Called(ctx, networkId, assetId, validatorId) - if len(ret) == 0 { - panic("no return value specified for GetValidator") - } - var r0 client.ApiGetValidatorRequest if rf, ok := ret.Get(0).(func(context.Context, string, string, string) client.ApiGetValidatorRequest); ok { r0 = rf(ctx, networkId, assetId, validatorId) @@ -39,10 +35,6 @@ func (_m *ValidatorsAPI) GetValidator(ctx context.Context, networkId string, ass func (_m *ValidatorsAPI) GetValidatorExecute(r client.ApiGetValidatorRequest) (*client.Validator, *http.Response, error) { ret := _m.Called(r) - if len(ret) == 0 { - panic("no return value specified for GetValidatorExecute") - } - var r0 *client.Validator var r1 *http.Response var r2 error @@ -78,10 +70,6 @@ func (_m *ValidatorsAPI) GetValidatorExecute(r client.ApiGetValidatorRequest) (* func (_m *ValidatorsAPI) ListValidators(ctx context.Context, networkId string, assetId string) client.ApiListValidatorsRequest { ret := _m.Called(ctx, networkId, assetId) - if len(ret) == 0 { - panic("no return value specified for ListValidators") - } - var r0 client.ApiListValidatorsRequest if rf, ok := ret.Get(0).(func(context.Context, string, string) client.ApiListValidatorsRequest); ok { r0 = rf(ctx, networkId, assetId) @@ -96,10 +84,6 @@ func (_m *ValidatorsAPI) ListValidators(ctx context.Context, networkId string, a func (_m *ValidatorsAPI) ListValidatorsExecute(r client.ApiListValidatorsRequest) (*client.ValidatorList, *http.Response, error) { ret := _m.Called(r) - if len(ret) == 0 { - panic("no return value specified for ListValidatorsExecute") - } - var r0 *client.ValidatorList var r1 *http.Response var r2 error