Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add another option to provide auth details #25

Merged
merged 1 commit into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. <br>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like how the examples don't assume the user placed the API Key at a specific location anymore. It's best to be explicit in the code where possible. However, I do think it's a bit more cumbersome for a user to manually copy/paste the key's two fields into the example (including a private key, which isn't ideal from a security perspective). Especially since the CDP GUI downloads the key onto the filesystem already.

What if we asked the user to specify the file location of the API Key? So maybe it looks like this?

const apiKeyLocation: string = './.coinbase_cloud_api_key.json';

const client = new StakingClient(apiKeyLocation);

This is just an idea, so curious what you think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends what we are trying to do here - something I probably failed calling out in the PR description.. you're right downloading an API key or copy/pasting these 2 values feel equally difficult - it felt like exchanging one problem with perhaps a bigger one 😅 . But the primary aim of moving to coy/pasting these 2 details - key name and private key is so that:

  1. We can move away from the coinbase_cloud api key file naming that the platform might take some time to update. We can skip the name confusion if we just copy/paste the relevant fields which the platform offers a good way of copy/pasting.

  2. I believe the platform team is working on a token based auth model at which point in time, copy/pasting a token from platform UI will be the expected path. So this PR in a way preps for that eventual future. @deangalvin-cb can maybe confirm/deny this or add more color.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Yes, we are working directly with the https://github.com/coinbase/coinbase-sdk-ruby team to see if we can better align & unify our designs. I had @drohit-cb go this route as to not spend too much time building each iteration, and just making this quick improvement now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kk I'm onboard 👍

These will be used in order to set up our client later in the example code. <br>
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:
Comment on lines +31 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can use fewer words here 🤔.

Suggested change
3. Get your API keys info such as api key name and api private key from here: https://portal.cdp.coinbase.com/access/api. <br>
These will be used in order to set up our client later in the example code. <br>
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:
3. Download an API Key from the [portal](https://portal.cdp.coinbase.com/access/api). For more information about API Keys, refer to [our docs](https://docs.cdp.coinbase.com/developer-platform/docs/cdp-keys).
4. Copy one of the code samples below into an `example.ts` file and run: :rocket:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tl;dr want to move away from downloading the file completely

```shell
ts-node example.ts
```
Expand All @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what would be really cool and nice to have is a "test" API key that perhaps has limited scopes etc that I can plaster in all examples. Could be easy for someone wanting to just run the example as is, without going to the platform.

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) => {
Expand Down
17 changes: 12 additions & 5 deletions examples/ethereum/create-and-process-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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',
);
}

Expand Down Expand Up @@ -80,7 +84,10 @@ async function stakePartialEth(): Promise<void> {
}

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 ...',
Expand Down
6 changes: 5 additions & 1 deletion examples/ethereum/create-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
if (stakerAddress === '') {
Expand Down
17 changes: 12 additions & 5 deletions examples/solana/create-and-process-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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',
);
}

Expand Down Expand Up @@ -83,7 +87,10 @@ async function stakeSolana(): Promise<void> {
}

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 ...',
Expand Down
6 changes: 5 additions & 1 deletion examples/solana/create-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
if (walletAddress === '') {
Expand Down
35 changes: 25 additions & 10 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> => {
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 {
Expand All @@ -35,7 +50,7 @@ export const buildJWT = async (

const header = {
alg: 'ES256',
kid: apiKey.name,
kid: keyName,
typ: 'JWT',
nonce: nonce(),
};
Expand All @@ -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
Expand Down Expand Up @@ -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 (
Expand Down
Loading
Loading