diff --git a/CHANGELOG.md b/CHANGELOG.md index be22b0c3..76d14985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/package-lock.json b/package-lock.json index 5585b8f9..911afa05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@coinbase/coinbase-sdk", - "version": "0.22.0", + "version": "0.23.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@coinbase/coinbase-sdk", - "version": "0.22.0", + "version": "0.23.0", "license": "ISC", "dependencies": { "@scure/bip32": "^1.4.0", diff --git a/package.json b/package.json index cae29094..9dcbf7b7 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/quickstart-template/package.json b/quickstart-template/package.json index 9ac33906..d8a4a74f 100644 --- a/quickstart-template/package.json +++ b/quickstart-template/package.json @@ -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" diff --git a/src/coinbase/address/external_address.ts b/src/coinbase/address/external_address.ts index 6d6d4ff2..dd975f0c 100644 --- a/src/coinbase/address/external_address.ts +++ b/src/coinbase/address/external_address.ts @@ -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 @@ -63,9 +63,11 @@ export class ExternalAddress extends Address { mode: StakeOptionsMode = StakeOptionsMode.DEFAULT, options: { [key: string]: string } = {}, ): Promise { - 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); } @@ -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.`); diff --git a/src/coinbase/address/wallet_address.ts b/src/coinbase/address/wallet_address.ts index f0e90cec..f56d07c1 100644 --- a/src/coinbase/address/wallet_address.ts +++ b/src/coinbase/address/wallet_address.ts @@ -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"; @@ -716,7 +716,8 @@ export class WalletAddress extends Address { timeoutSeconds = 600, intervalSeconds = 0.2, ): Promise { - 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( @@ -1010,7 +1011,8 @@ export class WalletAddress extends Address { timeoutSeconds: number, intervalSeconds: number, ): Promise { - 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."); } @@ -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(); } diff --git a/src/coinbase/staking_operation.ts b/src/coinbase/staking_operation.ts index 00f908f3..fefac629 100644 --- a/src/coinbase/staking_operation.ts +++ b/src/coinbase/staking_operation.ts @@ -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) + ); } /** @@ -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); + } +} diff --git a/src/tests/authenticator_test.ts b/src/tests/authenticator_test.ts index 8bc7cb58..d7d078b0 100644 --- a/src/tests/authenticator_test.ts +++ b/src/tests/authenticator_test.ts @@ -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", ); }); }); @@ -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", ); }); }); diff --git a/src/tests/external_address_test.ts b/src/tests/external_address_test.ts index ac56ca3b..2f590686 100644 --- a/src/tests/external_address_test.ts +++ b/src/tests/external_address_test.ts @@ -22,7 +22,11 @@ import { import Decimal from "decimal.js"; import { ExternalAddress } from "../coinbase/address/external_address"; import { StakeOptionsMode } from "../coinbase/types"; -import { ExecutionLayerWithdrawalOptionsBuilder, StakingOperation } from "../coinbase/staking_operation"; +import { + ConsensusLayerExitOptionBuilder, + ExecutionLayerWithdrawalOptionsBuilder, + StakingOperation, +} from "../coinbase/staking_operation"; import { Asset } from "../coinbase/asset"; import { randomUUID } from "crypto"; import { StakingReward } from "../coinbase/staking_reward"; @@ -35,7 +39,7 @@ describe("ExternalAddress", () => { const STAKING_CONTEXT_MODEL: StakingContextModel = { context: { stakeable_balance: { - amount: "3000000000000000000", + amount: "128000000000000000000", asset: { asset_id: Coinbase.assets.Eth, network_id: Coinbase.networks.EthereumHolesky, @@ -233,11 +237,87 @@ describe("ExternalAddress", () => { expect(op).toBeInstanceOf(StakingOperation); }); + describe("native eth staking", () => { + it("should successfully build an 0x01 stake operation", async () => { + Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL); + Coinbase.apiClients.stake!.buildStakingOperation = mockReturnValue(STAKING_OPERATION_MODEL); + Coinbase.apiClients.asset!.getAsset = getAssetMock(); + const op = await address.buildStakeOperation( + new Decimal("32"), + Coinbase.assets.Eth, + StakeOptionsMode.NATIVE, + { + withdrawal_credential_type: "0x01", + }, + ); + + expect(Coinbase.apiClients.stake!.getStakingContext).toHaveBeenCalledWith({ + address_id: address.getId(), + network_id: address.getNetworkId(), + asset_id: Coinbase.assets.Eth, + options: { + mode: StakeOptionsMode.NATIVE, + withdrawal_credential_type: "0x01", + }, + }); + expect(Coinbase.apiClients.stake!.buildStakingOperation).toHaveBeenCalledWith({ + address_id: address.getId(), + network_id: address.getNetworkId(), + asset_id: Coinbase.assets.Eth, + action: "stake", + options: { + mode: StakeOptionsMode.NATIVE, + amount: "32000000000000000000", + withdrawal_credential_type: "0x01", + }, + }); + + expect(op).toBeInstanceOf(StakingOperation); + }); + + it("should successfully build an 0x02 stake operation", async () => { + Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL); + Coinbase.apiClients.stake!.buildStakingOperation = mockReturnValue(STAKING_OPERATION_MODEL); + Coinbase.apiClients.asset!.getAsset = getAssetMock(); + const op = await address.buildStakeOperation( + new Decimal("64"), + Coinbase.assets.Eth, + StakeOptionsMode.NATIVE, + { + withdrawal_credential_type: "0x02", + }, + ); + + expect(Coinbase.apiClients.stake!.getStakingContext).toHaveBeenCalledWith({ + address_id: address.getId(), + network_id: address.getNetworkId(), + asset_id: Coinbase.assets.Eth, + options: { + mode: StakeOptionsMode.NATIVE, + withdrawal_credential_type: "0x02", + }, + }); + expect(Coinbase.apiClients.stake!.buildStakingOperation).toHaveBeenCalledWith({ + address_id: address.getId(), + network_id: address.getNetworkId(), + asset_id: Coinbase.assets.Eth, + action: "stake", + options: { + mode: StakeOptionsMode.NATIVE, + amount: "64000000000000000000", + withdrawal_credential_type: "0x02", + }, + }); + + expect(op).toBeInstanceOf(StakingOperation); + }); + }); + it("should return an error for not enough amount to stake", async () => { Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL); await expect( - address.buildStakeOperation(new Decimal("3.1"), Coinbase.assets.Eth), + address.buildStakeOperation(new Decimal("300"), Coinbase.assets.Eth), ).rejects.toThrow(Error); expect(Coinbase.apiClients.stake!.getStakingContext).toHaveBeenCalledWith({ address_id: address.getId(), @@ -334,6 +414,77 @@ describe("ExternalAddress", () => { expect(Coinbase.apiClients.stake!.buildStakingOperation).toHaveBeenCalledTimes(0); }); + describe("native eth consensus layer exits", () => { + it("should successfully build an unstake operation", async () => { + Coinbase.apiClients.stake!.buildStakingOperation = mockReturnValue(STAKING_OPERATION_MODEL); + Coinbase.apiClients.asset!.getAsset = getAssetMock(); + + const builder = new ConsensusLayerExitOptionBuilder(); + builder.addValidator("0x123"); + builder.addValidator("0x456"); + builder.addValidator("0x456"); + builder.addValidator("0x789"); + builder.addValidator("0x789"); + const options = await builder.build(); + + const op = await address.buildUnstakeOperation( + new Decimal("0"), + Coinbase.assets.Eth, + StakeOptionsMode.NATIVE, + options, + ); + + expect(Coinbase.apiClients.stake!.buildStakingOperation).toHaveBeenCalledWith({ + address_id: address.getId(), + network_id: address.getNetworkId(), + asset_id: Coinbase.assets.Eth, + action: "unstake", + options: { + mode: StakeOptionsMode.NATIVE, + unstake_type: "consensus", + validator_pub_keys: "0x123,0x456,0x789", + }, + }); + expect(op).toBeInstanceOf(StakingOperation); + }); + + it("should respect existing options", async () => { + Coinbase.apiClients.stake!.buildStakingOperation = mockReturnValue(STAKING_OPERATION_MODEL); + Coinbase.apiClients.asset!.getAsset = getAssetMock(); + + let options: { [key: string]: string } = { some_other_option: "value" }; + + const builder = new ConsensusLayerExitOptionBuilder(); + builder.addValidator("0x123"); + builder.addValidator("0x456"); + builder.addValidator("0x456"); + builder.addValidator("0x789"); + builder.addValidator("0x789"); + options = await builder.build(options); + + const op = await address.buildUnstakeOperation( + new Decimal("0"), + Coinbase.assets.Eth, + StakeOptionsMode.NATIVE, + options, + ); + + expect(Coinbase.apiClients.stake!.buildStakingOperation).toHaveBeenCalledWith({ + address_id: address.getId(), + network_id: address.getNetworkId(), + asset_id: Coinbase.assets.Eth, + action: "unstake", + options: { + mode: StakeOptionsMode.NATIVE, + some_other_option: "value", + unstake_type: "consensus", + validator_pub_keys: "0x123,0x456,0x789", + }, + }); + expect(op).toBeInstanceOf(StakingOperation); + }); + }); + describe("native eth execution layer withdrawals", () => { it("should successfully build an unstake operation", async () => { Coinbase.apiClients.stake!.buildStakingOperation = mockReturnValue(STAKING_OPERATION_MODEL); @@ -358,7 +509,7 @@ describe("ExternalAddress", () => { action: "unstake", options: { mode: StakeOptionsMode.NATIVE, - withdrawal_credential_type: "0x02", + unstake_type: "execution", validator_unstake_amounts: '{"0x123":"1000000000000000000000","0x456":"2000000000000000000000"}', }, @@ -391,7 +542,7 @@ describe("ExternalAddress", () => { options: { mode: StakeOptionsMode.NATIVE, some_other_option: "value", - withdrawal_credential_type: "0x02", + unstake_type: "execution", validator_unstake_amounts: '{"0x123":"1000000000000000000000"}', }, }); @@ -688,7 +839,7 @@ describe("ExternalAddress", () => { it("should return the stakeable balance successfully with default params", async () => { Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL); const stakeableBalance = await address.stakeableBalance(Coinbase.assets.Eth); - expect(stakeableBalance).toEqual(new Decimal("3")); + expect(stakeableBalance).toEqual(new Decimal("128")); expect(Coinbase.apiClients.stake!.getStakingContext).toHaveBeenCalledWith({ address_id: address.getId(), network_id: address.getNetworkId(), @@ -706,7 +857,7 @@ describe("ExternalAddress", () => { StakeOptionsMode.PARTIAL, {}, ); - expect(stakeableBalance).toEqual(new Decimal("3")); + expect(stakeableBalance).toEqual(new Decimal("128")); expect(Coinbase.apiClients.stake!.getStakingContext).toHaveBeenCalledWith({ address_id: address.getId(), network_id: address.getNetworkId(), diff --git a/src/tests/wallet_address_test.ts b/src/tests/wallet_address_test.ts index f81965ba..a549b2ad 100644 --- a/src/tests/wallet_address_test.ts +++ b/src/tests/wallet_address_test.ts @@ -70,6 +70,7 @@ import { Transaction } from "../coinbase/transaction"; import { WalletAddress } from "../coinbase/address/wallet_address"; import { Wallet } from "../coinbase/wallet"; import { + ConsensusLayerExitOptionBuilder, ExecutionLayerWithdrawalOptionsBuilder, StakingOperation, } from "../coinbase/staking_operation"; @@ -374,7 +375,7 @@ describe("WalletAddress", () => { const STAKING_CONTEXT_MODEL: StakingContextModel = { context: { stakeable_balance: { - amount: "3000000000000000000", + amount: "128000000000000000000", asset: { asset_id: Coinbase.assets.Eth, network_id: Coinbase.networks.EthereumHolesky, @@ -533,6 +534,82 @@ describe("WalletAddress", () => { expect(op).toBeInstanceOf(StakingOperation); }); + describe("native eth staking", () => { + it("should successfully create an 0x01 stake operation", async () => { + Coinbase.apiClients.asset!.getAsset = getAssetMock(); + Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL); + Coinbase.apiClients.walletStake!.createStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + Coinbase.apiClients.walletStake!.broadcastStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + STAKING_OPERATION_MODEL.status = StakingOperationStatusEnum.Complete; + Coinbase.apiClients.walletStake!.getStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + + const op = await walletAddress.createStake( + 32, + Coinbase.assets.Eth, + StakeOptionsMode.NATIVE, + { + withdrawal_credential_type: "0x01", + }, + ); + + expect(Coinbase.apiClients.walletStake!.createStakingOperation).toHaveBeenCalledWith( + walletAddress.getWalletId(), + walletAddress.getId(), + { + network_id: walletAddress.getNetworkId(), + asset_id: Coinbase.assets.Eth, + action: "stake", + options: { + mode: StakeOptionsMode.NATIVE, + amount: "32000000000000000000", + withdrawal_credential_type: "0x01", + }, + }, + ); + expect(op).toBeInstanceOf(StakingOperation); + }); + + it("should successfully create an 0x02 stake operation", async () => { + Coinbase.apiClients.asset!.getAsset = getAssetMock(); + Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL); + Coinbase.apiClients.walletStake!.createStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + Coinbase.apiClients.walletStake!.broadcastStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + STAKING_OPERATION_MODEL.status = StakingOperationStatusEnum.Complete; + Coinbase.apiClients.walletStake!.getStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + + const op = await walletAddress.createStake( + 64, + Coinbase.assets.Eth, + StakeOptionsMode.NATIVE, + { + withdrawal_credential_type: "0x02", + }, + ); + + expect(Coinbase.apiClients.walletStake!.createStakingOperation).toHaveBeenCalledWith( + walletAddress.getWalletId(), + walletAddress.getId(), + { + network_id: walletAddress.getNetworkId(), + asset_id: Coinbase.assets.Eth, + action: "stake", + options: { + mode: StakeOptionsMode.NATIVE, + amount: "64000000000000000000", + withdrawal_credential_type: "0x02", + }, + }, + ); + expect(op).toBeInstanceOf(StakingOperation); + }); + }); + it("should create a staking operation from the address but in failed status", async () => { Coinbase.apiClients.asset!.getAsset = getAssetMock(); Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL); @@ -594,6 +671,50 @@ describe("WalletAddress", () => { expect(op).toBeInstanceOf(StakingOperation); }); + describe("with native eth consensus layer exits", () => { + it("should create a staking operation from the address", async () => { + Coinbase.apiClients.asset!.getAsset = getAssetMock(); + Coinbase.apiClients.walletStake!.createStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + Coinbase.apiClients.walletStake!.broadcastStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + STAKING_OPERATION_MODEL.status = StakingOperationStatusEnum.Complete; + Coinbase.apiClients.walletStake!.getStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + + const builder = new ConsensusLayerExitOptionBuilder(); + builder.addValidator("0x123"); + builder.addValidator("0x456"); + builder.addValidator("0x456"); + builder.addValidator("0x789"); + builder.addValidator("0x789"); + const options = await builder.build(); + + const op = await walletAddress.createUnstake( + 0.001, + Coinbase.assets.Eth, + StakeOptionsMode.NATIVE, + options, + ); + + expect(Coinbase.apiClients.walletStake!.createStakingOperation).toHaveBeenCalledWith( + walletAddress.getWalletId(), + walletAddress.getId(), + { + network_id: walletAddress.getNetworkId(), + asset_id: Coinbase.assets.Eth, + action: "unstake", + options: { + mode: StakeOptionsMode.NATIVE, + unstake_type: "consensus", + validator_pub_keys: "0x123,0x456,0x789", + }, + }, + ); + expect(op).toBeInstanceOf(StakingOperation); + }); + }); + describe("with native eth execution layer withdrawals", () => { it("should create a staking operation from the address", async () => { Coinbase.apiClients.asset!.getAsset = getAssetMock(); @@ -626,7 +747,7 @@ describe("WalletAddress", () => { action: "unstake", options: { mode: StakeOptionsMode.NATIVE, - withdrawal_credential_type: "0x02", + unstake_type: "execution", validator_unstake_amounts: '{"0x123":"100000000000000000000","0x456":"200000000000000000000"}', }, @@ -659,7 +780,7 @@ describe("WalletAddress", () => { it("should return the stakeable balance successfully with default params", async () => { Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL); const stakeableBalance = await walletAddress.stakeableBalance(Coinbase.assets.Eth); - expect(stakeableBalance).toEqual(new Decimal("3")); + expect(stakeableBalance).toEqual(new Decimal("128")); }); });