diff --git a/README.md b/README.md index 6c11cf8..fd8ac2b 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,17 @@ Prerequisite: [Node 20+](https://www.npmjs.com/package/node/v/20.11.1) ```shell npm install @coinbase/staking-client-library-ts ``` -2. Create and download an API key from the [Coinbase Developer Platform](https://portal.cdp.coinbase.com/access/api). -3. Place the key named `.coinbase_cloud_api_key.json` at the root of this repository. -4. Install necessary Typescript dependencies: + +2. Install necessary Typescript dependencies: ```shell - npm install -g ts-node - npm install -g typescript - ``` -5. Copy and paste one of the code samples below or any of our [provided examples](./examples/) into an `example.ts` file and run it with `ts-node` :rocket: + npm install -g ts-node typescript + ``` + +3. Get your API keys info such as api key name and api private key from here: https://portal.cdp.coinbase.com/access/api.
+ These will be used in order to set up our client later in the example code.
+ For detailed instructions refer to our api key setup guide [here](https://docs.cdp.coinbase.com/developer-platform/docs/cdp-keys). + +4. Copy and paste one of the code samples below or any of our [provided examples](./examples/) into an `example.ts` file and run it with `ts-node` :rocket: ```shell ts-node example.ts ``` @@ -45,7 +48,11 @@ This code sample creates an ETH staking workflow. View the full code sample [her // examples/ethereum/create-workflow.ts import { StakingClient } from "@coinbase/staking-client-library-ts"; -const client = new StakingClient(); +// Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api +const apiKeyName: string = 'your-api-key-name'; +const apiPrivateKey: string = 'your-api-private-key'; + +const client = new StakingClient(apiKeyName, apiPrivateKey); client.Ethereum.stake('holesky', '0xdb816889F2a7362EF242E5a717dfD5B38Ae849FE', '123') .then((workflow) => { @@ -108,7 +115,11 @@ This code sample creates a SOL staking workflow. View the full code sample [here // examples/solana/create-workflow.ts import { StakingClient } from "@coinbase/staking-client-library-ts"; -const client = new StakingClient(); +// Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api +const apiKeyName: string = 'your-api-key-name'; +const apiPrivateKey: string = 'your-api-private-key'; + +const client = new StakingClient(apiKeyName, apiPrivateKey); client.Solana.stake('devnet', '8rMGARtkJY5QygP1mgvBFLsE9JrvXByARJiyNfcSE5Z', '100000000') .then((workflow) => { @@ -175,13 +186,17 @@ This code sample returns rewards for an Ethereum validator address. View the ful // examples/ethereum/list-rewards.ts import { StakingClient } from "@coinbase/staking-client-library-ts"; +// Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api +const apiKeyName: string = 'your-api-key-name'; +const apiPrivateKey: string = 'your-api-private-key'; + +const client = new StakingClient(apiKeyName, apiPrivateKey); + // Defines which address and rewards we want to see const address: string = '0xac53512c39d0081ca4437c285305eb423f474e6153693c12fbba4a3df78bcaa3422b31d800c5bea71c1b017168a60474'; const filter: string = `address='${address}' AND period_end_time > '2024-02-25T00:00:00Z' AND period_end_time < '2024-02-27T00:00:00Z'`; -const client = new StakingClient(); - // Loops through rewards array and prints each reward client.Ethereum.listRewards(filter).then((resp) => { resp.rewards!.forEach((reward) => { diff --git a/examples/ethereum/create-and-process-workflow.ts b/examples/ethereum/create-and-process-workflow.ts index e58fe4e..268a72b 100644 --- a/examples/ethereum/create-and-process-workflow.ts +++ b/examples/ethereum/create-and-process-workflow.ts @@ -9,19 +9,23 @@ import { import { Workflow } from '../../src/gen/coinbase/staking/orchestration/v1/workflow.pb'; import { calculateTimeDifference } from '../../src/utils/date'; -const privateKey: string = ''; // replace with your private key +const walletPrivateKey: string = 'your-wallet-private-key'; // replace with your wallet's private key const stakerAddress: string = '0xdb816889F2a7362EF242E5a717dfD5B38Ae849FE'; // replace with your staker address const amount: string = '123'; // replace with your amount const network: string = 'holesky'; // replace with your network -const client = new StakingClient(); +// Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api +const apiKeyName: string = 'your-api-key-name'; +const apiPrivateKey: string = 'your-api-private-key'; + +const client = new StakingClient(apiKeyName, apiPrivateKey); const signer = TxSignerFactory.getSigner('ethereum'); async function stakePartialEth(): Promise { - if (privateKey === '' || stakerAddress === '') { + if (walletPrivateKey === '' || stakerAddress === '') { throw new Error( - 'Please set the privateKey and stakerAddress variables in this file', + 'Please set the walletPrivateKey and stakerAddress variables in this file', ); } @@ -80,7 +84,10 @@ async function stakePartialEth(): Promise { } console.log('Signing unsigned tx %s ...', unsignedTx); - const signedTx = await signer.signTransaction(privateKey, unsignedTx); + const signedTx = await signer.signTransaction( + walletPrivateKey, + unsignedTx, + ); console.log( 'Please broadcast this signed tx %s externally and return back the tx hash via the PerformWorkflowStep API ...', diff --git a/examples/ethereum/create-workflow.ts b/examples/ethereum/create-workflow.ts index ef80354..9102818 100644 --- a/examples/ethereum/create-workflow.ts +++ b/examples/ethereum/create-workflow.ts @@ -5,7 +5,11 @@ const stakerAddress: string = '0xdb816889F2a7362EF242E5a717dfD5B38Ae849FE'; // r const amount: string = '123'; // replace with your amount const network: string = 'holesky'; // replace with your network -const client = new StakingClient(); +// Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api +const apiKeyName: string = 'your-api-key-name'; +const apiPrivateKey: string = 'your-api-private-key'; + +const client = new StakingClient(apiKeyName, apiPrivateKey); async function stakePartialEth(): Promise { if (stakerAddress === '') { diff --git a/examples/solana/create-and-process-workflow.ts b/examples/solana/create-and-process-workflow.ts index 0e8e7ed..0bd1cb9 100644 --- a/examples/solana/create-and-process-workflow.ts +++ b/examples/solana/create-and-process-workflow.ts @@ -9,19 +9,23 @@ import { import { Workflow } from '../../src/gen/coinbase/staking/orchestration/v1/workflow.pb'; import { calculateTimeDifference } from '../../src/utils/date'; -const privateKey: string = ''; // replace with your private key +const walletPrivateKey: string = 'your-wallet-private-key'; // replace with your wallet's private key const walletAddress: string = ''; // replace with your wallet address const amount: string = '100000000'; // replace with your amount. For solana it should be >= 0.1 SOL const network: string = 'mainnet'; // replace with your network -const client = new StakingClient(); +// Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api +const apiKeyName: string = 'your-api-key-name'; +const apiPrivateKey: string = 'your-api-private-key'; + +const client = new StakingClient(apiKeyName, apiPrivateKey); const signer = TxSignerFactory.getSigner('solana'); async function stakeSolana(): Promise { - if (privateKey === '' || walletAddress === '') { + if (walletPrivateKey === '' || walletAddress === '') { throw new Error( - 'Please set the privateKey and walletAddress variables in this file', + 'Please set the walletPrivateKey and walletAddress variables in this file', ); } @@ -83,7 +87,10 @@ async function stakeSolana(): Promise { } console.log('Signing unsigned tx %s ...', unsignedTx); - const signedTx = await signer.signTransaction(privateKey, unsignedTx); + const signedTx = await signer.signTransaction( + walletPrivateKey, + unsignedTx, + ); console.log( 'Please broadcast this signed tx %s externally and return back the tx hash via the PerformWorkflowStep API ...', diff --git a/examples/solana/create-workflow.ts b/examples/solana/create-workflow.ts index 2cb97bb..8f007f5 100644 --- a/examples/solana/create-workflow.ts +++ b/examples/solana/create-workflow.ts @@ -5,7 +5,11 @@ const walletAddress: string = '9NL2SkpcsdyZwsG8NmHGNra4i4NSyKbJTVd9fUQ7kJHR'; // const amount: string = '100000000'; // replace with your amount. For solana it should be >= 0.1 SOL const network: string = 'mainnet'; // replace with your network -const client = new StakingClient(); +// Set your api key name and private key here. Get your keys from here: https://portal.cdp.coinbase.com/access/api +const apiKeyName: string = 'your-api-key-name'; +const apiPrivateKey: string = 'your-api-private-key'; + +const client = new StakingClient(apiKeyName, apiPrivateKey); async function stakeSolana(): Promise { if (walletAddress === '') { diff --git a/src/auth/index.ts b/src/auth/index.ts index 3b22df7..20023a6 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -8,20 +8,35 @@ const pemFooter = '-----END EC PRIVATE KEY-----'; /** * Build a JWT for the specified service and URI. - * @param service The name of the service. - * @param uri The URI for which the JWT is to be generated. * @returns The generated JWT. + * @param url The URL for which the JWT is to be generated. + * @param method The HTTP method for the request. + * @param apiKeyName The name of the API key. + * @param apiPrivateKey The private key present in the API key downloaded from platform. */ export const buildJWT = async ( url: string, method = 'GET', + apiKeyName?: string, + apiPrivateKey?: string, ): Promise => { - const keyFile = readFileSync('.coinbase_cloud_api_key.json', { - encoding: 'utf8', - }); - const apiKey: APIKey = JSON.parse(keyFile); + let pemPrivateKey: string; + let keyName: string; + let apiKey: APIKey; + + if (apiKeyName && apiPrivateKey) { + pemPrivateKey = extractPemKey(apiPrivateKey); + keyName = apiKeyName; + } else { + const keyFile = readFileSync('.coinbase_cloud_api_key.json', { + encoding: 'utf8', + }); + + apiKey = JSON.parse(keyFile); + pemPrivateKey = extractPemKey(apiKey.privateKey); + keyName = apiKey.name; + } - const pemPrivateKey = extractPemKey(apiKey.privateKey); let privateKey: JWK.Key; try { @@ -35,7 +50,7 @@ export const buildJWT = async ( const header = { alg: 'ES256', - kid: apiKey.name, + kid: keyName, typ: 'JWT', nonce: nonce(), }; @@ -44,7 +59,7 @@ export const buildJWT = async ( const uri = `${method} ${url.substring(8)}`; const claims: APIKeyClaims = { - sub: apiKey.name, + sub: keyName, iss: 'coinbase-cloud', nbf: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 60, // +1 minute @@ -103,7 +118,7 @@ interface APIKeyClaims { */ const extractPemKey = (privateKeyString: string): string => { // Remove all newline characters - privateKeyString = privateKeyString.replace(/\n/g, ''); + privateKeyString = privateKeyString.replace(/\\n|\n/g, ''); // If the string starts with the standard PEM header and footer, return as is. if ( diff --git a/src/client/staking-client.ts b/src/client/staking-client.ts index eb5f53c..dd82571 100644 --- a/src/client/staking-client.ts +++ b/src/client/staking-client.ts @@ -47,17 +47,27 @@ const DEFAULT_URL = 'https://api.developer.coinbase.com/staking'; export class StakingClient { readonly baseURL: string; + readonly apiKeyName: string | undefined; + readonly apiPrivateKey: string | undefined; readonly Ethereum: Ethereum; readonly Solana: Solana; readonly Cosmos: Cosmos; - constructor(baseURL?: string) { + constructor(apiKeyName?: string, apiPrivateKey?: string, baseURL?: string) { if (baseURL) { this.baseURL = baseURL; } else { this.baseURL = DEFAULT_URL; } + if (apiKeyName) { + this.apiKeyName = apiKeyName; + } + + if (apiPrivateKey) { + this.apiPrivateKey = apiPrivateKey; + } + this.Ethereum = new Ethereum(this); this.Solana = new Solana(this); this.Cosmos = new Cosmos(this); @@ -70,7 +80,13 @@ export class StakingClient { const url: string = this.baseURL + '/orchestration'; // Generate the JWT token and get the auth details as a initReq object. - const initReq = await getAuthDetails(url, path, method); + const initReq = await getAuthDetails( + url, + path, + method, + this.apiKeyName, + this.apiPrivateKey, + ); const req: ListProtocolsRequest = {}; @@ -85,7 +101,13 @@ export class StakingClient { const url: string = this.baseURL + '/orchestration'; // Generate the JWT token and get the auth details as a initReq object. - const initReq = await getAuthDetails(url, path, method); + const initReq = await getAuthDetails( + url, + path, + method, + this.apiKeyName, + this.apiPrivateKey, + ); const req: ListNetworksRequest = { parent: parent, @@ -105,8 +127,14 @@ export class StakingClient { const url: string = this.baseURL + '/orchestration'; // Generate the JWT token and get the auth details as a initReq object. - const initReq = await getAuthDetails(url, path, method); - + // Generate the JWT token and get the auth details as a initReq object. + const initReq = await getAuthDetails( + url, + path, + method, + this.apiKeyName, + this.apiPrivateKey, + ); const req: ListActionsRequest = { parent: parent, }; @@ -125,7 +153,14 @@ export class StakingClient { const url: string = this.baseURL + '/orchestration'; // Generate the JWT token and get the auth details as a initReq object. - const initReq = await getAuthDetails(url, path, method); + // Generate the JWT token and get the auth details as a initReq object. + const initReq = await getAuthDetails( + url, + path, + method, + this.apiKeyName, + this.apiPrivateKey, + ); return StakingService.ViewStakingContext(req, initReq); } @@ -138,7 +173,14 @@ export class StakingClient { const url: string = this.baseURL + '/orchestration'; // Generate the JWT token and get the auth details as a initReq object. - const initReq = await getAuthDetails(url, path, method); + // Generate the JWT token and get the auth details as a initReq object. + const initReq = await getAuthDetails( + url, + path, + method, + this.apiKeyName, + this.apiPrivateKey, + ); return StakingService.CreateWorkflow(req, initReq); } @@ -151,7 +193,14 @@ export class StakingClient { const url: string = this.baseURL + '/orchestration'; // Generate the JWT token and get the auth details as a initReq object. - const initReq = await getAuthDetails(url, path, method); + // Generate the JWT token and get the auth details as a initReq object. + const initReq = await getAuthDetails( + url, + path, + method, + this.apiKeyName, + this.apiPrivateKey, + ); const req: GetWorkflowRequest = { name: name, @@ -172,7 +221,14 @@ export class StakingClient { const url: string = this.baseURL + '/orchestration'; // Generate the JWT token and get the auth details as a initReq object. - const initReq = await getAuthDetails(url, path, method); + // Generate the JWT token and get the auth details as a initReq object. + const initReq = await getAuthDetails( + url, + path, + method, + this.apiKeyName, + this.apiPrivateKey, + ); const req: PerformWorkflowStepRequest = { name: name, @@ -193,7 +249,14 @@ export class StakingClient { const url: string = this.baseURL + '/orchestration'; // Generate the JWT token and get the auth details as a initReq object. - const initReq = await getAuthDetails(url, path, method); + // Generate the JWT token and get the auth details as a initReq object. + const initReq = await getAuthDetails( + url, + path, + method, + this.apiKeyName, + this.apiPrivateKey, + ); const req: ListWorkflowsRequest = { pageSize: pageSize, @@ -214,7 +277,14 @@ export class StakingClient { const url: string = this.baseURL + '/rewards'; // Generate the JWT token and get the auth details as a initReq object. - const initReq = await getAuthDetails(url, path, method); + // Generate the JWT token and get the auth details as a initReq object. + const initReq = await getAuthDetails( + url, + path, + method, + this.apiKeyName, + this.apiPrivateKey, + ); return RewardService.ListRewards(req, initReq); } @@ -230,7 +300,14 @@ export class StakingClient { const url: string = this.baseURL + '/rewards'; // Generate the JWT token and get the auth details as a initReq object. - const initReq = await getAuthDetails(url, path, method); + // Generate the JWT token and get the auth details as a initReq object. + const initReq = await getAuthDetails( + url, + path, + method, + this.apiKeyName, + this.apiPrivateKey, + ); return RewardService.ListStakes(req, initReq); } @@ -265,9 +342,11 @@ async function getAuthDetails( url: string, path: string, method: string, + apiKeyName?: string, + apiPrivateKey?: string, ): Promise { // Generate the JWT token - const token = await buildJWT(url + path, method); + const token = await buildJWT(url + path, method, apiKeyName, apiPrivateKey); return { pathPrefix: url,