diff --git a/.github/workflows/test.yaml b/.github/workflows/ci.yaml similarity index 76% rename from .github/workflows/test.yaml rename to .github/workflows/ci.yaml index 4c06f2f..f14e9ca 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/ci.yaml @@ -1,4 +1,4 @@ -name: Go Test +name: CI on: push: @@ -12,7 +12,8 @@ permissions: contents: read jobs: - test: + ci: + name: ci runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -25,5 +26,11 @@ jobs: - name: Build run: go build -v ./... + - name: Lint + uses: golangci/golangci-lint-action@v4 + with: + version: v1.54.2 + args: --timeout=3m + - name: Test uses: robherley/go-test-action@v0 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml deleted file mode 100644 index 610eff2..0000000 --- a/.github/workflows/lint.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Go Lint - -on: - push: - branches: - - master - # We only run tests when we open PRs (and not for ex: on every commit) - # to avoid running workflows too frequently and incurring costs - pull_request: - -permissions: - contents: read - -jobs: - lint: - name: lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: "1.20" - cache: false - - name: golangci-lint - uses: golangci/golangci-lint-action@v4 - with: - version: v1.54.2 - args: --timeout=3m diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..f9c60fd --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,47 @@ +name: Version 🔖 + +on: + push: + branches: + - main + paths: + - '.version' + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + runs-on: ubuntu-latest + environment: release + permissions: + contents: write + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get version from .version file + id: release_version + run: echo "VERSION=$(cat .version")" >> $GITHUB_OUTPUT + + - name: Check if tag exists + id: check_tag + run: | + git fetch --tags + if git rev-parse "v${{ steps.release_version.outputs.VERSION }}" >/dev/null 2>&1; then + echo "::set-output name=EXISTS::true" + fi + + - name: Create Release + if: steps.check_tag.outputs.EXISTS != 'true' + uses: softprops/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.release_version.outputs.VERSION }} + name: Release v${{ steps.release_version.outputs.VERSION }} + draft: false + prerelease: false + generate_release_notes: true + make_latest: true diff --git a/.version b/.version new file mode 100644 index 0000000..8f0916f --- /dev/null +++ b/.version @@ -0,0 +1 @@ +0.5.0 diff --git a/README.md b/README.md index 87323ff..3538a20 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,6 @@ func main() { Parameters: &api.EthereumKilnStakingParameters_StakeParameters{ StakeParameters: &api.EthereumKilnStakeParameters{ StakerAddress: "0xdb816889F2a7362EF242E5a717dfD5B38Ae849FE", - IntegratorContractAddress: "0xA55416de5DE61A0AC1aa8970a280E04388B1dE4b", Amount: &api.Amount{ Value: "20", Currency: "ETH", @@ -91,7 +90,82 @@ func main() { Output ```text - 2024/03/28 11:43:49 Workflow created: projects/62376b2f-3f24-42c9-9025-d576a3c06d6f/workflows/ffbf9b45-c57b-49cb-a4d5-fdab66d8cb25 + 2024/03/28 11:43:49 Workflow created: workflows/ffbf9b45-c57b-49cb-a4d5-fdab66d8cb25 + ``` + + + +### Stake SOL :diamond_shape_with_a_dot_inside: + +This code sample creates an SOL staking workflow. View the full code sample [here](examples/solana/create-workflow/main.go) + +
+ Code Sample + +```golang +// examples/solana/create-workflow/main.go +package main + +import ( + "context" + "log" + + "github.com/coinbase/staking-client-library-go/auth" + "github.com/coinbase/staking-client-library-go/client" + "github.com/coinbase/staking-client-library-go/client/options" + api "github.com/coinbase/staking-client-library-go/gen/go/coinbase/staking/orchestration/v1" +) + +func main() { + ctx := context.Background() + + // Loads the API key from the default location. + apiKey, err := auth.NewAPIKey(auth.WithLoadAPIKeyFromFile(true)) + if err != nil { + log.Fatalf("error loading API key: %s", err.Error()) + } + + // Creates the Coinbase Staking API client. + stakingClient, err := client.New(ctx, options.WithAPIKey(apiKey)) + if err != nil { + log.Fatalf("error instantiating staking client: %s", err.Error()) + } + + req := &api.CreateWorkflowRequest{ + Workflow: &api.Workflow{ + Action: "protocols/solana/networks/devnet/actions/stake", + StakingParameters: &api.Workflow_SolanaStakingParameters{ + SolanaStakingParameters: &api.SolanaStakingParameters{ + Parameters: &api.SolanaStakingParameters_StakeParameters{ + StakeParameters: &api.SolanaStakeParameters{ + WalletAddress: walletAddress, + Amount: &api.Amount{ + Value: amount, + Currency: currency, + }, + }, + }, + }, + }, + }, + } + + workflow, err := stakingClient.Orchestration.CreateWorkflow(ctx, req) + if err != nil { + log.Fatalf("couldn't create workflow: %s", err.Error()) + } + + log.Printf("Workflow created: %s", workflow.Name) +} +``` + +
+ +
+ Output + + ```text + 2024/03/28 11:43:49 Workflow created: workflows/6bd9fd82-8b9d-4a49-9039-f95bb850a7a2 ```
diff --git a/examples/ethereum/create-and-process-workflow/main.go b/examples/ethereum/create-and-process-workflow/main.go index fbf0fff..b772285 100644 --- a/examples/ethereum/create-and-process-workflow/main.go +++ b/examples/ethereum/create-and-process-workflow/main.go @@ -25,10 +25,9 @@ const ( privateKey = "" // TODO: Replace with your staker addresses and amount. - stakerAddress = "" - integratorContractAddress = "0xA55416de5DE61A0AC1aa8970a280E04388B1dE4b" - amount = "11" - currency = "ETH" + stakerAddress = "" + amount = "11" + currency = "ETH" ) // An example function to demonstrate how to use the staking client libraries. @@ -59,8 +58,7 @@ func main() { EthereumKilnStakingParameters: &api.EthereumKilnStakingParameters{ Parameters: &api.EthereumKilnStakingParameters_StakeParameters{ StakeParameters: &api.EthereumKilnStakeParameters{ - StakerAddress: stakerAddress, - IntegratorContractAddress: integratorContractAddress, + StakerAddress: stakerAddress, Amount: &api.Amount{ Value: amount, Currency: currency, diff --git a/examples/ethereum/create-workflow/main.go b/examples/ethereum/create-workflow/main.go index 216f365..eb48f6b 100644 --- a/examples/ethereum/create-workflow/main.go +++ b/examples/ethereum/create-workflow/main.go @@ -8,6 +8,8 @@ import ( "context" "log" + "google.golang.org/protobuf/encoding/protojson" + "github.com/coinbase/staking-client-library-go/auth" "github.com/coinbase/staking-client-library-go/client" "github.com/coinbase/staking-client-library-go/client/options" @@ -36,8 +38,7 @@ func main() { EthereumKilnStakingParameters: &api.EthereumKilnStakingParameters{ Parameters: &api.EthereumKilnStakingParameters_StakeParameters{ StakeParameters: &api.EthereumKilnStakeParameters{ - StakerAddress: "0xdb816889F2a7362EF242E5a717dfD5B38Ae849FE", - IntegratorContractAddress: "0xA55416de5DE61A0AC1aa8970a280E04388B1dE4b", + StakerAddress: "0xdb816889F2a7362EF242E5a717dfD5B38Ae849FE", Amount: &api.Amount{ Value: "20", Currency: "ETH", @@ -54,5 +55,10 @@ func main() { log.Fatalf("couldn't create workflow: %s", err.Error()) } - log.Printf("Workflow created: %s", workflow.Name) + marshaled, err := protojson.MarshalOptions{Indent: " ", Multiline: true}.Marshal(workflow) + if err != nil { + log.Fatalf("error marshaling reward: %s", err.Error()) + } + + log.Printf("Workflow created: \n%s", marshaled) } diff --git a/examples/solana/create-and-process-workflow/main.go b/examples/solana/create-and-process-workflow/main.go new file mode 100644 index 0000000..9ef5115 --- /dev/null +++ b/examples/solana/create-and-process-workflow/main.go @@ -0,0 +1,159 @@ +/* + * This example code, demonstrates staking client library usage for performing e2e staking on Solana. + */ + +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/coinbase/staking-client-library-go/auth" + "github.com/coinbase/staking-client-library-go/client" + stakingerrors "github.com/coinbase/staking-client-library-go/client/errors" + "github.com/coinbase/staking-client-library-go/client/options" + "github.com/coinbase/staking-client-library-go/client/orchestration" + api "github.com/coinbase/staking-client-library-go/gen/go/coinbase/staking/orchestration/v1" + "github.com/coinbase/staking-client-library-go/internal/signer" +) + +const ( + // TODO: Replace with your private key. + privateKey = "" + + // TODO: Replace with your wallet addresses and amount. + walletAddress = "" + amount = "100000000" + currency = "SOL" +) + +// An example function to demonstrate how to use the staking client libraries. +func main() { + ctx := context.Background() + + apiKey, err := auth.NewAPIKey(auth.WithLoadAPIKeyFromFile(true)) + if err != nil { + log.Fatalf("error loading API key: %s", err.Error()) + } + + authOpt := options.WithAPIKey(apiKey) + + // Create a staking client. + stakingClient, err := client.New(ctx, authOpt) + if err != nil { + log.Fatalf("error instantiating staking client: %s", err.Error()) + } + + if privateKey == "" || walletAddress == "" { + log.Fatalf("privateKey and walletAddress must be set") + } + + req := &api.CreateWorkflowRequest{ + Workflow: &api.Workflow{ + Action: "protocols/solana/networks/devnet/actions/stake", + StakingParameters: &api.Workflow_SolanaStakingParameters{ + SolanaStakingParameters: &api.SolanaStakingParameters{ + Parameters: &api.SolanaStakingParameters_StakeParameters{ + StakeParameters: &api.SolanaStakeParameters{ + WalletAddress: walletAddress, + Amount: &api.Amount{ + Value: amount, + Currency: currency, + }, + }, + }, + }, + }, + }, + } + + workflow, err := stakingClient.Orchestration.CreateWorkflow(ctx, req) + if err != nil { + sae := stakingerrors.FromError(err) + _ = sae.Print() + os.Exit(1) + } + + log.Printf("Workflow created %s ...\n", workflow.Name) + + // Run loop until workflow reaches a terminal state + for { + // Get the latest workflow state + workflow, err = stakingClient.Orchestration.GetWorkflow(ctx, &api.GetWorkflowRequest{Name: workflow.Name}) + if err != nil { + log.Fatalf(fmt.Errorf("error getting workflow: %w", err).Error()) + } + + printWorkflowProgressDetails(workflow) + + // If workflow is in WAITING_FOR_EXT_BROADCAST state, sign, broadcast the transaction and update the workflow. + if orchestration.WorkflowWaitingForExternalBroadcast(workflow) { + unsignedTx := workflow.Steps[workflow.GetCurrentStepId()].GetTxStepOutput().GetUnsignedTx() + + // Logic to sign the transaction. This can be substituted with any other signing mechanism. + log.Printf("Signing unsigned tx %s ...\n", unsignedTx) + + signedTx, err := signer.New("solana").SignTransaction([]string{privateKey}, &signer.UnsignedTx{Data: []byte(unsignedTx)}) + if err != nil { + log.Fatalf(fmt.Errorf("error signing transaction: %w", err).Error()) + } + + // Add logic to broadcast the tx here. + fmt.Printf("Please broadcast this signed tx %s externally and return back the tx hash via the PerformWorkflowStep API ...\n", signedTx) + break + } else if orchestration.WorkflowFinished(workflow) { + break + } + + // Sleep for 1 second before polling for workflow status again + time.Sleep(1 * time.Second) + } +} + +func printWorkflowProgressDetails(workflow *api.Workflow) { + if len(workflow.GetSteps()) <= 0 { + fmt.Println("Waiting for steps to be created ...") + time.Sleep(2 * time.Second) + } + + step := workflow.Steps[workflow.GetCurrentStepId()] + + createTime := workflow.GetCreateTime().AsTime() + updateTime := workflow.GetUpdateTime().AsTime() + runtime := updateTime.Sub(createTime) + + var stepDetails string + + switch step.GetOutput().(type) { + case *api.WorkflowStep_TxStepOutput: + stepDetails = fmt.Sprintf("state: %s tx hash: %s", + step.GetTxStepOutput().GetState().String(), + step.GetTxStepOutput().GetTxHash(), + ) + case *api.WorkflowStep_WaitStepOutput: + stepDetails = fmt.Sprintf("state: %s current: %d target: %d", + step.GetWaitStepOutput().GetState().String(), + step.GetWaitStepOutput().GetCurrent(), + step.GetWaitStepOutput().GetTarget(), + ) + } + + if orchestration.WorkflowFinished(workflow) { + log.Printf("Workflow reached end state - step name: %s %s workflow state: %s runtime: %v\n", + step.GetName(), + stepDetails, + workflow.GetState().String(), + runtime, + ) + } else { + log.Printf("Waiting for workflow to finish - step name: %s %s workflow state: %s runtime: %v\n", + step.GetName(), + stepDetails, + workflow.GetState().String(), + runtime, + ) + } +} diff --git a/examples/solana/create-workflow/main.go b/examples/solana/create-workflow/main.go index e1dd815..da39e6d 100644 --- a/examples/solana/create-workflow/main.go +++ b/examples/solana/create-workflow/main.go @@ -6,61 +6,49 @@ package main import ( "context" - "fmt" "log" - "os" - "time" + + "google.golang.org/protobuf/encoding/protojson" "github.com/coinbase/staking-client-library-go/auth" "github.com/coinbase/staking-client-library-go/client" stakingerrors "github.com/coinbase/staking-client-library-go/client/errors" "github.com/coinbase/staking-client-library-go/client/options" - "github.com/coinbase/staking-client-library-go/client/orchestration" api "github.com/coinbase/staking-client-library-go/gen/go/coinbase/staking/orchestration/v1" - "github.com/coinbase/staking-client-library-go/internal/signer" + "os" ) const ( - // TODO: Replace with your private key. - privateKey = "" - // TODO: Replace with your wallet addresses and amount. - walletAddress = "" - validatorAddress = "GkqYQysEGmuL6V2AJoNnWZUz2ZBGWhzQXsJiXm2CLKAN" - amount = "100000000" - currency = "SOL" + walletAddress = "8rMGARtkJY5QygP1mgvBFLsE9JrvXByARJiyNfcSE5Z" + amount = "100000000" + currency = "SOL" ) // An example function to demonstrate how to use the staking client libraries. func main() { ctx := context.Background() + // Loads the API key from the default location. apiKey, err := auth.NewAPIKey(auth.WithLoadAPIKeyFromFile(true)) if err != nil { log.Fatalf("error loading API key: %s", err.Error()) } - authOpt := options.WithAPIKey(apiKey) - - // Create a staking client. - stakingClient, err := client.New(ctx, authOpt) + // Creates the Coinbase Staking API client. + stakingClient, err := client.New(ctx, options.WithAPIKey(apiKey)) if err != nil { log.Fatalf("error instantiating staking client: %s", err.Error()) } - if privateKey == "" || walletAddress == "" { - log.Fatalf("privateKey and walletAddress must be set") - } - req := &api.CreateWorkflowRequest{ Workflow: &api.Workflow{ - Action: "protocols/solana/networks/testnet/actions/stake", + Action: "protocols/solana/networks/devnet/actions/stake", StakingParameters: &api.Workflow_SolanaStakingParameters{ SolanaStakingParameters: &api.SolanaStakingParameters{ Parameters: &api.SolanaStakingParameters_StakeParameters{ StakeParameters: &api.SolanaStakeParameters{ - WalletAddress: walletAddress, - ValidatorAddress: validatorAddress, + WalletAddress: walletAddress, Amount: &api.Amount{ Value: amount, Currency: currency, @@ -79,83 +67,10 @@ func main() { os.Exit(1) } - log.Printf("Workflow created %s ...\n", workflow.Name) - - // Run loop until workflow reaches a terminal state - for { - // Get the latest workflow state - workflow, err = stakingClient.Orchestration.GetWorkflow(ctx, &api.GetWorkflowRequest{Name: workflow.Name}) - if err != nil { - log.Fatalf(fmt.Errorf("error getting workflow: %w", err).Error()) - } - - printWorkflowProgressDetails(workflow) - - // If workflow is in WAITING_FOR_EXT_BROADCAST state, sign, broadcast the transaction and update the workflow. - if orchestration.WorkflowWaitingForExternalBroadcast(workflow) { - unsignedTx := workflow.Steps[workflow.GetCurrentStepId()].GetTxStepOutput().GetUnsignedTx() - - // Logic to sign the transaction. This can be substituted with any other signing mechanism. - log.Printf("Signing unsigned tx %s ...\n", unsignedTx) - - signedTx, err := signer.New("solana").SignTransaction([]string{privateKey}, &signer.UnsignedTx{Data: []byte(unsignedTx)}) - if err != nil { - log.Fatalf(fmt.Errorf("error signing transaction: %w", err).Error()) - } - - // Add logic to broadcast the tx here. - fmt.Printf("Please broadcast this signed tx %s externally and return back the tx hash via the PerformWorkflowStep API ...\n", signedTx) - break - } else if orchestration.WorkflowFinished(workflow) { - break - } - - // Sleep for 1 second before polling for workflow status again - time.Sleep(1 * time.Second) - } -} - -func printWorkflowProgressDetails(workflow *api.Workflow) { - if len(workflow.GetSteps()) <= 0 { - fmt.Println("Waiting for steps to be created ...") - time.Sleep(2 * time.Second) - } - - step := workflow.Steps[workflow.GetCurrentStepId()] - - createTime := workflow.GetCreateTime().AsTime() - updateTime := workflow.GetUpdateTime().AsTime() - runtime := updateTime.Sub(createTime) - - var stepDetails string - - switch step.GetOutput().(type) { - case *api.WorkflowStep_TxStepOutput: - stepDetails = fmt.Sprintf("state: %s tx hash: %s", - step.GetTxStepOutput().GetState().String(), - step.GetTxStepOutput().GetTxHash(), - ) - case *api.WorkflowStep_WaitStepOutput: - stepDetails = fmt.Sprintf("state: %s current: %d target: %d", - step.GetWaitStepOutput().GetState().String(), - step.GetWaitStepOutput().GetCurrent(), - step.GetWaitStepOutput().GetTarget(), - ) + marshaled, err := protojson.MarshalOptions{Indent: " ", Multiline: true}.Marshal(workflow) + if err != nil { + log.Fatalf("error marshaling reward: %s", err.Error()) } - if orchestration.WorkflowFinished(workflow) { - log.Printf("Workflow reached end state - step name: %s %s workflow state: %s runtime: %v\n", - step.GetName(), - stepDetails, - workflow.GetState().String(), - runtime, - ) - } else { - log.Printf("Waiting for workflow to finish - step name: %s %s workflow state: %s runtime: %v\n", - step.GetName(), - stepDetails, - workflow.GetState().String(), - runtime, - ) - } + log.Printf("Workflow created: \n%s", marshaled) }