Skip to content

Commit

Permalink
Add solana staking
Browse files Browse the repository at this point in the history
  • Loading branch information
drohit-cb committed Sep 9, 2024
1 parent a98e1f9 commit 997e84c
Show file tree
Hide file tree
Showing 6 changed files with 365 additions and 119 deletions.
26 changes: 11 additions & 15 deletions CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,20 @@ those capabilities for the Go SDK:
## Developer Wallets

| Concept | Base-Sepolia | Base-Mainnet | Ethereum-Holesky | Ethereum-Mainnet |
| ------------- | :----------: | :----------: | :--------------: | :--------------: |
| Addresses |||||
| Send |||||
| Trade |||||
| Faucet |||||
| Server-Signer |||||
| Stake [^1] |||||

[^1]: Currently only available for Shared ETH Staking.
|---------------|:------------:|:------------:|:----------------:|:----------------:|
| Addresses |||||
| Send |||||
| Trade |||||
| Faucet |||||
| Server-Signer |||||
| Stake |||||

## End-User Wallets

| Concept | Base-Sepolia | Base-Mainnet | Ethereum-Holesky | Ethereum-Mainnet |
| ------------------ | :----------: | :----------: | :--------------: | :--------------: |
| External Addresses |||||
| Stake [^2] |||||

[^2]: Dedicated ETH Staking is currently only available on Testnet (Ethereum-Holesky).
| Concept | Base-Sepolia | Base-Mainnet | Ethereum-Holesky | Ethereum-Mainnet | Solana-Devnet |
|--------------------|:------------:|:------------:|:----------------:|:----------------:|:-------------:|
| External Addresses ||||||
| Stake ||||||

## Testnet vs. Mainnet

Expand Down
177 changes: 142 additions & 35 deletions examples/build_staking_operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,178 @@ package main

import (
"context"
"fmt"
"log"
"math/big"
"os"
"time"

api "github.com/coinbase/coinbase-sdk-go/gen/client"
"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"
walletAddress = "9NL2SkpcsdyZwsG8NmHGNra4i4NSyKbJTVd9fUQ7kJHR"
amount = big.NewFloat(0.1)
rpcURL = "SOLANA_DEVNET_RPC_URL"
)

func main() {
ctx := context.Background()

client, err := coinbase.NewClient(
coinbase.WithAPIKeyFromJSON(os.Args[1]),
coinbase.WithAPIKeyFromJSON("/Users/rohit/code/cb/staking/.coinbase_cloud_api_key_dev.json"),
coinbase.WithBaseURL("http://localhost:8004"),
)
if err != nil {
log.Fatalf("error creating coinbase client: %v", err)
}

address := coinbase.NewExternalAddress("ethereum-holesky", "0x57a063e1df096aaA6b2068C3C7FE6Ac4BC3c4F58")
op, err := client.BuildStakeOperation(ctx, big.NewFloat(0.0001), coinbase.Eth, address)
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", op.ID())
for _, transaction := range op.Transactions() {
log.Printf("staking operation Transaction: %+v\n", transaction)

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(), []string{os.Getenv("SOL_PRIVATE_KEY")})
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")
}

address = coinbase.NewExternalAddress(
"ethereum-mainnet",
"0xddb00798137e9e7cc89f1e9679e6ce6ea580b8f9",
)
signers := make([]solana.PrivateKey, 0, len(privateKeys))

rewards, err := client.ListStakingRewards(
ctx,
coinbase.Eth,
[]coinbase.Address{*address},
time.Now().Add(-7*24*time.Hour),
time.Now(),
api.STAKINGREWARDFORMAT_USD,
)
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 {
log.Fatalf("error listing rewards: %v", err)
return "", err
}

for _, reward := range rewards {
println(reward.ToJSON())
// 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)
}

address = coinbase.NewExternalAddress(
"ethereum-mainnet",
"0xadbf3776d60b3f9dd30cb3257b50583898745deb40cb6cb842120753bf055f6c3863e0f5bdb5c403d9aa5a275ce165e8",
)
balances, err := client.ListHistoricalStakingBalances(
ctx,
coinbase.Eth,
address,
time.Now().Add(-7*24*time.Hour),
time.Now(),
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 {
log.Fatalf("error listing balances: %v", err)
return "", fmt.Errorf("failed to send transaction: %w", err)
}

for _, balance := range balances {
println(balance.String())
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 ""
}
38 changes: 30 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,61 @@ module github.com/coinbase/coinbase-sdk-go
go 1.22.5

require (
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce
github.com/ethereum/go-ethereum v1.14.8
github.com/gagliardetto/binary v0.8.0
github.com/gagliardetto/solana-go v1.10.0
github.com/stretchr/testify v1.9.0
gopkg.in/square/go-jose.v2 v2.6.0
gopkg.in/validator.v2 v2.0.1
)

require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
filippo.io/edwards25519 v1.0.0-rc.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
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/consensys/bavard v0.1.13 // indirect
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/go-ole/go-ole v1.3.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/gagliardetto/treeout v0.1.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/rpc v1.2.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
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
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/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
golang.org/x/time v0.5.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)
Loading

0 comments on commit 997e84c

Please sign in to comment.