Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

## [0.23.0] - 2025-04-16

- Add support for both consensus and execution based withdrawals post-pectra.

## [0.22.0] - 2025-04-02

- Add `ExecutionLayerWithdrawalOptionsBuilder` to allow for native ETH execution layer withdrawals as defined in https://eips.ethereum.org/EIPS/eip-7002.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"license": "ISC",
"description": "Coinbase Platform SDK",
"repository": "https://github.com/coinbase/coinbase-sdk-nodejs",
"version": "0.22.0",
"version": "0.23.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion quickstart-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"dependencies": {
"@solana/web3.js": "^2.0.0-rc.1",
"bs58": "^6.0.0",
"@coinbase/coinbase-sdk": "^0.22.0",
"@coinbase/coinbase-sdk": "^0.23.0",
"csv-parse": "^5.5.6",
"csv-writer": "^1.6.0",
"viem": "^2.21.6"
Expand Down
9 changes: 6 additions & 3 deletions src/coinbase/address/external_address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Amount, BroadcastExternalTransactionResponse, StakeOptionsMode } from "
import { Coinbase } from "../coinbase";
import Decimal from "decimal.js";
import { Asset } from "../asset";
import { HasWithdrawalCredentialType0x02Option, StakingOperation } from "../staking_operation";
import { IsDedicatedEthUnstakeV2Operation, StakingOperation } from "../staking_operation";

/**
* A representation of a blockchain Address, which is a user-controlled account on a Network. Addresses are used to
Expand Down Expand Up @@ -63,9 +63,11 @@ export class ExternalAddress extends Address {
mode: StakeOptionsMode = StakeOptionsMode.DEFAULT,
options: { [key: string]: string } = {},
): Promise<StakingOperation> {
if (!HasWithdrawalCredentialType0x02Option(options)) {
// If performing a native eth unstake v2, validation is always performed server-side.
if (!IsDedicatedEthUnstakeV2Operation(assetId, "unstake", mode, options)) {
await this.validateCanUnstake(amount, assetId, mode, options);
}

return this.buildStakingOperation(amount, assetId, "unstake", mode, options);
}

Expand Down Expand Up @@ -117,7 +119,8 @@ export class ExternalAddress extends Address {

newOptions.mode = mode;

if (!HasWithdrawalCredentialType0x02Option(options)) {
// If performing a native eth unstake v2, the amount is not required.
if (!IsDedicatedEthUnstakeV2Operation(assetId, action, mode, newOptions)) {
const stakingAmount = new Decimal(amount.toString());
if (stakingAmount.lessThanOrEqualTo(0)) {
throw new Error(`Amount required greater than zero.`);
Expand Down
11 changes: 7 additions & 4 deletions src/coinbase/address/wallet_address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from "../types";
import { delay } from "../utils";
import { Wallet as WalletClass } from "../wallet";
import { HasWithdrawalCredentialType0x02Option, StakingOperation } from "../staking_operation";
import { IsDedicatedEthUnstakeV2Operation, StakingOperation } from "../staking_operation";
import { PayloadSignature } from "../payload_signature";
import { SmartContract } from "../smart_contract";
import { FundOperation } from "../fund_operation";
Expand Down Expand Up @@ -716,7 +716,8 @@ export class WalletAddress extends Address {
timeoutSeconds = 600,
intervalSeconds = 0.2,
): Promise<StakingOperation> {
if (!HasWithdrawalCredentialType0x02Option(options)) {
// If performing a native ETH unstake, validation is always performed server-side.
if (!IsDedicatedEthUnstakeV2Operation(assetId, "unstake", mode, options)) {
await this.validateCanUnstake(amount, assetId, mode, options);
}
return this.createStakingOperation(
Expand Down Expand Up @@ -1010,7 +1011,8 @@ export class WalletAddress extends Address {
timeoutSeconds: number,
intervalSeconds: number,
): Promise<StakingOperation> {
if (!HasWithdrawalCredentialType0x02Option(options)) {
// If performing a native ETH unstake, the amount is not required.
if (!IsDedicatedEthUnstakeV2Operation(assetId, action, mode, options)) {
if (new Decimal(amount.toString()).lessThanOrEqualTo(0)) {
throw new Error("Amount required greater than zero.");
}
Expand Down Expand Up @@ -1078,7 +1080,8 @@ export class WalletAddress extends Address {

options.mode = mode ? mode : StakeOptionsMode.DEFAULT;

if (!HasWithdrawalCredentialType0x02Option(options)) {
// If performing a native ETH unstake, the amount is not required.
if (!IsDedicatedEthUnstakeV2Operation(assetId, action, mode, options)) {
options.amount = asset.toAtomicAmount(new Decimal(amount.toString())).toString();
}

Expand Down
74 changes: 67 additions & 7 deletions src/coinbase/staking_operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,47 @@ import {
import { Transaction } from "./transaction";
import { Coinbase } from "./coinbase";
import { delay } from "./utils";
import { Amount } from "./types";
import { Amount, StakeOptionsMode } from "./types";
import { Asset } from "./asset";
import Decimal from "decimal.js";

export const WithdrawalCredentialType0x02 = "0x02";
export const UnstakeTypeExecution = "execution";
export const UnstakeTypeConsensus = "consensus";

/**
* Checks if the given options contain the withdrawal credential type 0x02.
* Checks if the given options contains the unstake type option.
*
* @param options - An object containing various options.
* @returns True if the withdrawal credential type is 0x02, false otherwise.
* @returns True if the unstake type is consensus or execution, false otherwise.
*/
export function HasWithdrawalCredentialType0x02Option(options: { [key: string]: string }): boolean {
return options["withdrawal_credential_type"] === WithdrawalCredentialType0x02;
export function HasUnstakeTypeOption(options: { [key: string]: string }): boolean {
return (
options["unstake_type"] === UnstakeTypeConsensus ||
options["unstake_type"] === UnstakeTypeExecution
);
}

/**
* Determines if the given parameters represent a native ETH unstake operation (version 2).
*
* @param assetId - The ID of the asset.
* @param action - The action being performed.
* @param mode - The mode of the stake options.
* @param options - An object containing various options.
* @returns True if the parameters represent a native ETH unstake operation (version 2), false otherwise.
*/
export function IsDedicatedEthUnstakeV2Operation(
assetId: string,
action: string,
mode: string,
options: { [key: string]: string },
): boolean {
return (
assetId === Coinbase.assets.Eth &&
action == "unstake" &&
mode === StakeOptionsMode.NATIVE &&
HasUnstakeTypeOption(options)
);
}

/**
Expand Down Expand Up @@ -340,10 +367,43 @@ export class ExecutionLayerWithdrawalOptionsBuilder {
}

const executionLayerWithdrawalOptions = {
withdrawal_credential_type: WithdrawalCredentialType0x02,
unstake_type: UnstakeTypeExecution,
validator_unstake_amounts: JSON.stringify(validatorAmounts),
};

return Object.assign({}, options, executionLayerWithdrawalOptions);
}
}

/**
* A builder class for creating consensus layer exit options.
*/
export class ConsensusLayerExitOptionBuilder {
private validatorPubKeys: string[] = [];

/**
* Adds a validator public key to the list of validators.
*
* @param pubKey - The public key of the validator.
*/
addValidator(pubKey: string) {
if (!this.validatorPubKeys.includes(pubKey)) {
this.validatorPubKeys.push(pubKey);
}
}

/**
* Builds the consensus layer exit options.
*
* @param options - Existing options to merge with the built options.
* @returns A promise that resolves to an object containing the consensus layer exit options merged with any provided options.
*/
async build(options: { [key: string]: string } = {}): Promise<{ [key: string]: string }> {
const consensusLayerExitOptions = {
unstake_type: UnstakeTypeConsensus,
validator_pub_keys: this.validatorPubKeys.join(","),
};

return Object.assign({}, options, consensusLayerExitOptions);
}
}
4 changes: 2 additions & 2 deletions src/tests/authenticator_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe("Authenticator tests", () => {
const config = await authenticator.authenticateRequest(VALID_CONFIG, true);
const correlationContext = config.headers["Correlation-Context"] as string;
expect(correlationContext).toContain(
"sdk_version=0.22.0,sdk_language=typescript,source=mockSource",
"sdk_version=0.23.0,sdk_language=typescript,source=mockSource",
);
});
});
Expand Down Expand Up @@ -204,7 +204,7 @@ describe("Authenticator tests for Edwards key", () => {
const config = await authenticator.authenticateRequest(VALID_CONFIG, true);
const correlationContext = config.headers["Correlation-Context"] as string;
expect(correlationContext).toContain(
"sdk_version=0.22.0,sdk_language=typescript,source=mockSource",
"sdk_version=0.23.0,sdk_language=typescript,source=mockSource",
);
});
});
Expand Down
Loading