diff --git a/README.md b/README.md index 06c4f2c..15ca572 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Staking API Typescript Client Library +[![npm version](https://badge.fury.io/js/@coinbase%2Fstaking-client-library-ts.svg)](https://badge.fury.io/js/@coinbase%2Fstaking-client-library-ts) + This repository contains the Protocol Buffer definitions for the Coinbase **Staking API**, as well as the Typescript client libraries generated from them. ## Overview @@ -64,16 +66,16 @@ To test that your API Key gives you access as expected to the Staking APIs: ## Running example from your application -1. Build the packages in this repo with `npm install && npm run build` -2. Install this package in your application - `npm install ` -3. Add your API key to the root of your application as `.coinbase_cloud_api_key.json` -4. Run example code: +1. Install this package in your application - `npm install @coinbase/staking-client-library-ts` +2. Add your API key to the root of your application as `.coinbase_cloud_api_key.json` +3. Run example code: ```typescript - import { StakingServiceClient } from "../staking-client-library-ts"; + import { StakingServiceClient } from "@coinbase/staking-client-library-ts"; + + const client = new StakingServiceClient(); const exampleFunction = () => { - const client = new StakingServiceClient(); client.listProtocols().then((response) => { console.log(response); }); diff --git a/package-lock.json b/package-lock.json index 4bd6852..5fab32f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,17 @@ { "name": "@coinbase/staking-client-library-ts", - "version": "1.0.0", + "version": "0.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@coinbase/staking-client-library-ts", - "version": "1.0.0", + "version": "0.3.3", "license": "Apache-2.0", + "dependencies": { + "@ethereumjs/tx": "^5.1.0", + "node-jose": "^2.2.0" + }, "devDependencies": { "@types/node-jose": "^1.1.10", "@typescript-eslint/eslint-plugin": "^6.7.0", @@ -15,7 +19,6 @@ "eslint": "^8.49.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", - "node-jose": "^2.2.0", "prettier": "^3.0.3", "rimraf": "^3.0.2", "typescript": "^5.2.2" @@ -86,6 +89,68 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@ethereumjs/common": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-4.1.0.tgz", + "integrity": "sha512-XWdQvUjlQHVwh4uGEPFKHpsic69GOsMXEhlHrggS5ju/+2zAmmlz6B25TkCCymeElC9DUp13tH5Tc25Iuvtlcg==", + "dependencies": { + "@ethereumjs/util": "^9.0.1", + "crc": "^4.3.2" + } + }, + "node_modules/@ethereumjs/rlp": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-5.0.1.tgz", + "integrity": "sha512-Ab/Hfzz+T9Zl+65Nkg+9xAmwKPLicsnQ4NW49pgvJp9ovefuic95cgOS9CbPc9izIEgsqm1UitV0uNveCvud9w==", + "bin": { + "rlp": "bin/rlp.cjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethereumjs/tx": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-5.1.0.tgz", + "integrity": "sha512-VUhw2+4yXArJZRWhPjmZFrN4WUjUo0qUZUszVpW2KzsGlqCFf67kwJcH9Rca5eS0CRHjr2qHJLpvYOjNuaXVdA==", + "dependencies": { + "@ethereumjs/common": "^4.1.0", + "@ethereumjs/rlp": "^5.0.1", + "@ethereumjs/util": "^9.0.1", + "ethereum-cryptography": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "c-kzg": "^2.1.2" + }, + "peerDependenciesMeta": { + "c-kzg": { + "optional": true + } + } + }, + "node_modules/@ethereumjs/util": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-9.0.1.tgz", + "integrity": "sha512-NdFFEzCc3H1sYkNnnySwLg6owdQMhjUc2jfuDyx8Xv162WSluCnnSKouKOSG3njGNEyy2I9NmF8zTRDwuqpZWA==", + "dependencies": { + "@ethereumjs/rlp": "^5.0.1", + "ethereum-cryptography": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "c-kzg": "^2.1.2" + }, + "peerDependenciesMeta": { + "c-kzg": { + "optional": true + } + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -119,6 +184,28 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -174,6 +261,39 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@scure/base": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", + "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "dependencies": { + "@noble/curves": "~1.1.0", + "@noble/hashes": "~1.3.1", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -476,7 +596,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -496,7 +615,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -548,7 +666,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, "funding": [ { "type": "github", @@ -632,6 +749,22 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/crc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/crc/-/crc-4.3.2.tgz", + "integrity": "sha512-uGDHf4KLLh2zsHa8D8hIQ1H/HtFQhyHrc0uhHBcoKGol/Xnb+MPYfUMw7cvON6ze/GUESTudKayDcJC5HnJv1A==", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "buffer": ">=6.0.3" + }, + "peerDependenciesMeta": { + "buffer": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -742,8 +875,7 @@ "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "dev": true + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -939,6 +1071,17 @@ "node": ">=0.10.0" } }, + "node_modules/ethereum-cryptography": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz", + "integrity": "sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==", + "dependencies": { + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@scure/bip32": "1.3.1", + "@scure/bip39": "1.2.1" + } + }, "node_modules/execa": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", @@ -1196,7 +1339,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -1449,8 +1591,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -1461,8 +1602,7 @@ "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", - "dev": true + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, "node_modules/lru-cache": { "version": "6.0.0", @@ -1544,7 +1684,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, "engines": { "node": ">= 6.13.0" } @@ -1553,7 +1692,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-jose/-/node-jose-2.2.0.tgz", "integrity": "sha512-XPCvJRr94SjLrSIm4pbYHKLEaOsDvJCpyFw/6V/KK/IXmyZ6SFBzAUDO9HQf4DB/nTEFcRGH87mNciOP23kFjw==", - "dev": true, "dependencies": { "base64url": "^3.0.1", "buffer": "^6.0.3", @@ -1685,8 +1823,7 @@ "node_modules/pako": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", - "dev": true + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -1794,7 +1931,6 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, "engines": { "node": ">= 0.6.0" } @@ -2211,7 +2347,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "dev": true, "bin": { "uuid": "dist/bin/uuid" } diff --git a/package.json b/package.json index 73d807c..0e3c1bc 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,9 @@ { "name": "@coinbase/staking-client-library-ts", - "version": "0.3.2", + "version": "0.4.0", "description": "Coinbase Cloud Staking API Typescript Library", "repository": "https://github.com/coinbase/staking-client-library-ts.git", "license": "Apache-2.0", - "main": "dist/index.js", - "types": "dist/index.d.ts", "scripts": { "clean": "rimraf ./dist", "build": "npm run clean && tsc", @@ -22,7 +20,8 @@ "dist/" ], "dependencies": { - "node-jose": "^2.2.0" + "node-jose": "^2.2.0", + "@ethereumjs/tx": "^5.1.0" }, "devDependencies": { "@types/node-jose": "^1.1.10", @@ -34,5 +33,7 @@ "prettier": "^3.0.3", "typescript": "^5.2.2", "rimraf": "^3.0.2" - } + }, + "main": "dist/index.js", + "types": "dist/index.d.ts" } diff --git a/src/client/staking-service-client.ts b/src/client/staking-service-client.ts index c166002..7be5f68 100644 --- a/src/client/staking-service-client.ts +++ b/src/client/staking-service-client.ts @@ -25,6 +25,7 @@ import { PerformWorkflowStepRequest, RefreshWorkflowStepRequest, Workflow, + WorkflowState, } from "../gen/coinbase/staking/v1alpha1/workflow.pb"; import { EthereumKiln } from "./protocols/ethereum_kiln_staking"; @@ -143,18 +144,21 @@ export class StakingServiceClient { // Return back a signed tx or a broadcasted tx hash for a given workflow and step number. async performWorkflowStep( - workflowName: string, + projectId: string, + workflowId: string, stepIndex: number, - data?: string, + data: string, ): Promise { - const path: string = `/api/v1alpha1/${parent}/workflows`; + const parent: string = `projects/${projectId}`; + const name: string = `${parent}/workflows/${workflowId}`; + const path: string = `/api/v1alpha1/${name}/step`; const method: string = "POST"; // Generate the JWT token and get the auth details as a initReq object. const initReq = await getAuthDetails(this.baseURL, path, method); const req: PerformWorkflowStepRequest = { - name: workflowName, + name: name, step: stepIndex, data, }; @@ -164,17 +168,20 @@ export class StakingServiceClient { // Refresh a workflow step given its workflow name and step number. async refreshWorkflowStep( - workflowName: string, + projectId: string, + workflowId: string, stepIndex: number, ): Promise { - const path: string = `/api/v1alpha1/${parent}/workflows`; + const parent: string = `projects/${projectId}`; + const name: string = `${parent}/workflows/${workflowId}`; + const path: string = `/api/v1alpha1/${name}/refresh`; const method: string = "POST"; // Generate the JWT token and get the auth details as a initReq object. const initReq = await getAuthDetails(this.baseURL, path, method); const req: RefreshWorkflowStepRequest = { - name: workflowName, + name: name, step: stepIndex, }; @@ -204,6 +211,29 @@ export class StakingServiceClient { } } +export function workflowHasFinished(workflow: Workflow): boolean { + return ( + workflow.state === WorkflowState.STATE_COMPLETED || + workflow.state === WorkflowState.STATE_FAILED || + workflow.state === WorkflowState.STATE_CANCELED || + workflow.state === WorkflowState.STATE_CANCEL_FAILED + ); +} + +export function workflowWaitingForSigning(workflow: Workflow): boolean { + return workflow.state === WorkflowState.STATE_WAITING_FOR_SIGNING; +} + +export function workflowWaitingForExternalBroadcast( + workflow: Workflow, +): boolean { + return workflow.state === WorkflowState.STATE_WAITING_FOR_EXT_BROADCAST; +} + +export function workflowFailedRefreshable(workflow: Workflow): boolean { + return workflow.state === WorkflowState.STATE_FAILED_REFRESHABLE; +} + async function getAuthDetails( baseURL: string, path: string, diff --git a/src/examples/public/example_e2e_staking.ts b/src/examples/public/example_e2e_staking.ts new file mode 100644 index 0000000..bc7f3ff --- /dev/null +++ b/src/examples/public/example_e2e_staking.ts @@ -0,0 +1,159 @@ +import { TxSignerFactory } from "../../signers"; +import { + StakingServiceClient, + workflowHasFinished, + workflowWaitingForSigning, + workflowWaitingForExternalBroadcast, +} from "../../client/staking-service-client"; +import { Workflow } from "../../gen/coinbase/staking/v1alpha1/workflow.pb"; +import { calculateTimeDifference } from "../../utils/date"; + +const projectId: string = ""; // replace with your project id +const privateKey: string = ""; // replace with your private key +const stakerAddress: string = ""; // replace with your staker address +const integrationAddress: string = "0x0a868e4e07a0a00587a783720b76fad9f7eea009"; // replace with your integration address +const amount: string = "123"; // replace with your amount + +const client = new StakingServiceClient(); + +const signer = TxSignerFactory.getSigner("ethereum"); + +async function stakePartialEth(): Promise { + if (projectId === "" || privateKey === "" || stakerAddress === "") { + throw new Error( + "Please set the projectId, privateKey and stakerAddress variables in this file", + ); + } + + let unsignedTx = ""; + let workflow: Workflow = {} as Workflow; + let currentStepId: number | undefined; + let workflowId: string; + + try { + // Create a new eth kiln stake workflow + workflow = await client.EthereumKiln.stake( + projectId, + "goerli", + false, + stakerAddress, + integrationAddress, + amount, + ); + + workflowId = workflow.name?.split("/").pop() || ""; + if (workflowId == null || workflowId === "") { + throw new Error("Unexpected workflow state. workflowId is null"); + } + + currentStepId = workflow.currentStepId; + if (currentStepId == null) { + throw new Error("Unexpected workflow state. currentStepId is null"); + } + + console.log("Workflow created %s ...", workflow.name); + } catch (error: any) { + throw new Error(`Error creating workflow ${error.message}`); + } + + // Loop until the workflow has reached an end state. + // eslint-disable-next-line no-constant-condition + while (true) { + // Every second, get the latest workflow state. + // If the workflow is waiting for signing, sign the unsigned tx and return back the signed tx. + // If the workflow is waiting for external broadcast, sign and broadcast the unsigned tx externally and return back the tx hash via the PerformWorkflowStep API. + // Note: In this example, we just log this message as the wallet provider needs to implement this logic. + try { + workflow = await client.getWorkflow(projectId, workflowId); + } catch (error: any) { + // TODO: add retry logic for network errors + throw new Error(`Error getting workflow ${error.message}`); + } + + await printWorkflowProgressDetails(workflow); + + if (workflowWaitingForSigning(workflow)) { + unsignedTx = + workflow.steps![currentStepId].txStepOutput?.unsignedTx || ""; + if (unsignedTx === "") { + console.log("Waiting for unsigned tx to be available ..."); + await new Promise((resolve) => setTimeout(resolve, 1000)); // sleep for 1 second + continue; + } + + console.log("Signing unsigned tx %s ...", unsignedTx); + const signedTx = await signer.signTransaction(privateKey, unsignedTx); + console.log("Returning back signed tx %s ...", signedTx); + console.warn("HALTING"); + + workflow = await client.performWorkflowStep( + projectId, + workflowId, + currentStepId, + signedTx, + ); + } else if (workflowWaitingForExternalBroadcast(workflow)) { + console.log( + "Please sign and broadcast this unsigned tx %s externally and return back the tx hash via the PerformWorkflowStep API ...", + unsignedTx, + ); + break; + } else if (workflowHasFinished(workflow)) { + console.log("Workflow completed with state %s ...", workflow.state); + break; + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); // sleep for 1 second + } +} + +async function printWorkflowProgressDetails(workflow: Workflow): Promise { + if (workflow.steps == null || workflow.steps.length === 0) { + console.log("Waiting for steps to be created ..."); + await new Promise((resolve) => setTimeout(resolve, 1000)); // sleep for 1 second + return; + } + + const currentStepId = workflow.currentStepId; + if (currentStepId == null) { + return; + } + + const step = workflow.steps[currentStepId]; + + const txStepDetails = `state: ${step.txStepOutput?.state} tx hash: ${step.txStepOutput?.txHash}`; + + // TODO(rohit) add support later + // const waitStepDetails = `state: ${step.waitStepOutput?.state}} current: ${step.waitStepOutput?.current}} target: ${step.waitStepOutput?.target}` + + const runtime = calculateTimeDifference( + workflow.createTime, + workflow.updateTime, + ); + + if (workflowHasFinished(workflow)) { + console.log( + "Workflow reached end state - step name: %s %s workflow state: %s runtime: %d seconds", + step.name, + txStepDetails, + workflow.state, + runtime, + ); + } else { + console.log( + "Waiting for workflow to finish - step name: %s %s workflow state: %s runtime: %d seconds", + step.name, + txStepDetails, + workflow.state, + runtime, + ); + } +} + +stakePartialEth() + .then(() => { + console.log("Done staking eth"); + }) + .catch((error) => { + console.error("Error staking eth: ", error.message); + }); diff --git a/src/index.ts b/src/index.ts index a967f98..091e243 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,5 @@ +import * as Utils from "./utils/date"; + +export { Utils }; + export { StakingServiceClient } from "./client/staking-service-client"; diff --git a/src/signers/ethereum_signer.ts b/src/signers/ethereum_signer.ts new file mode 100644 index 0000000..626bbca --- /dev/null +++ b/src/signers/ethereum_signer.ts @@ -0,0 +1,26 @@ +import { TransactionFactory } from "@ethereumjs/tx"; +import { bytesToHex } from "@ethereumjs/util"; +import { TxSigner } from "./txsigner"; + +export class EthereumTransactionSigner implements TxSigner { + async signTransaction( + privateKey: string, + unsignedTx: string, + ): Promise { + let transaction = TransactionFactory.fromSerializedData( + Buffer.from(unsignedTx, "hex"), + ); + + const decodedPrivateKey = Buffer.from(privateKey, "hex"); + + let signedTx = transaction.sign(decodedPrivateKey); + + const verifiedSignature = signedTx.verifySignature(); + + if (!verifiedSignature) { + throw new Error("Produced an invalid signature!"); + } + + return bytesToHex(signedTx.serialize()); + } +} diff --git a/src/signers/index.ts b/src/signers/index.ts new file mode 100644 index 0000000..8dc7b48 --- /dev/null +++ b/src/signers/index.ts @@ -0,0 +1,2 @@ +export * from "./ethereum_signer"; +export * from "./txsigner"; diff --git a/src/signers/txsigner.ts b/src/signers/txsigner.ts new file mode 100644 index 0000000..16f4200 --- /dev/null +++ b/src/signers/txsigner.ts @@ -0,0 +1,18 @@ +import { EthereumTransactionSigner } from "./ethereum_signer"; + +export interface TxSigner { + // eslint-disable-next-line no-unused-vars + signTransaction(privateKey: string, unsignedTx: string): Promise; +} + +export class TxSignerFactory { + static getSigner(protocol: string): TxSigner { + switch (protocol) { + case "ethereum": + return new EthereumTransactionSigner(); + // other cases for additional protocols + default: + throw new Error("Unsupported protocol"); + } + } +} diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 0000000..1acec6c --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,10 @@ +// calculateTimeDifference is a function that takes two timestamps and returns the difference in seconds. +export function calculateTimeDifference( + timestamp1: string, + timestamp2: string, +): number { + const date1 = new Date(timestamp1); + const date2 = new Date(timestamp2); + + return Math.abs(date1.getTime() - date2.getTime()) / 1000; +} diff --git a/tsconfig.json b/tsconfig.json index b04ed21..f49258b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,15 @@ { "compilerOptions": { "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "module": "CommonJS", /* Specify what module code is generated. */ + "module": "CommonJS", /* Specify what module code is generated. */ "outDir": "./dist", "rootDir": "./src", "sourceMap": true, "declaration": true, - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - "strict": true, /* Enable all strict type-checking options. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "strict": true, /* Enable all strict type-checking options. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "openapi"]