From a913f5525320386194f5a4856e7fc298cc0d7185 Mon Sep 17 00:00:00 2001 From: Filipe Azevedo Date: Thu, 12 Sep 2024 12:51:12 +0100 Subject: [PATCH] add solana example --- examples/{ => eth}/build_staking_operation.go | 19 +- examples/solana/build_staking_operation.go | 191 ++++++++++++++++++ 2 files changed, 192 insertions(+), 18 deletions(-) rename examples/{ => eth}/build_staking_operation.go (77%) create mode 100644 examples/solana/build_staking_operation.go diff --git a/examples/build_staking_operation.go b/examples/eth/build_staking_operation.go similarity index 77% rename from examples/build_staking_operation.go rename to examples/eth/build_staking_operation.go index 7ca2e46..beaa9c1 100644 --- a/examples/build_staking_operation.go +++ b/examples/eth/build_staking_operation.go @@ -13,7 +13,6 @@ import ( func main() { ctx := context.Background() - client, err := coinbase.NewClient( coinbase.WithAPIKeyFromJSON(os.Args[1]), ) @@ -22,27 +21,11 @@ func main() { } address := coinbase.NewExternalAddress("ethereum-holesky", "0x57a063e1df096aaA6b2068C3C7FE6Ac4BC3c4F58") - - 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), - ) + op, err := client.BuildStakeOperation(ctx, big.NewFloat(0.0001), coinbase.Eth, address) 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) } diff --git a/examples/solana/build_staking_operation.go b/examples/solana/build_staking_operation.go new file mode 100644 index 0000000..1d58b76 --- /dev/null +++ b/examples/solana/build_staking_operation.go @@ -0,0 +1,191 @@ +package main + +import ( + "context" + "fmt" + "log" + "math/big" + "os" + "path/filepath" + "strings" + "time" + + "github.com/btcsuite/btcutil/base58" + "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 = "solana-devnet" + amount = big.NewFloat(0.1) + rpcURL = "https://api.devnet.solana.com" + defaultPrivKeyPath = filepath.Join(home(), ".config/solana/id.json") +) + +func main() { + ctx := context.Background() + + walletAddress := os.Args[2] + + privKeys := []string{defaultPrivKeyPath} + if keys := os.Getenv("SOL_PRIVATE_KEYS"); keys != "" { + privKeys = strings.Split(keys, ",") + } + + signers := make([]solana.PrivateKey, len(privKeys)) + for i, pk := range privKeys { + privKey, err := solana.PrivateKeyFromSolanaKeygenFile(pk) + if err != nil { + log.Fatalf("private key %s does not exist or is invalid", privKey) + } + + signers[i] = privKey + } + + client, err := coinbase.NewClient( + coinbase.WithAPIKeyFromJSON(os.Args[1]), + ) + if err != nil { + log.Fatalf("error creating coinbase client: %v", err) + } + + address := coinbase.NewExternalAddress(networkID, walletAddress) + + 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()) + + for _, transaction := range stakingOperation.Transactions() { + log.Printf("Tx unsigned payload: %s\n\n", transaction.UnsignedPayload()) + + signedTx, err := signSolTransaction(transaction.UnsignedPayload(), signers) + 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, signers []solana.PrivateKey) (string, error) { + 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{ + 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 "" +} + +func home() string { + home, err := os.UserHomeDir() + if err != nil { + log.Fatal("unable to get user homedir") + } + + return home +}