From 524a5724fc89770d992f40032d9da4a1978ca70b Mon Sep 17 00:00:00 2001 From: Ryan Gilbert Date: Thu, 17 Apr 2025 18:17:53 -0400 Subject: [PATCH 1/2] Update publish.yml (#408) --- .github/workflows/publish.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 68949510..2496e79c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,7 +1,6 @@ name: Publish Package to NPM on: - release: - types: [published] + workflow_dispatch: jobs: build: runs-on: ubuntu-latest From dda118494df08cedad1dfbc8b419d90342da307e Mon Sep 17 00:00:00 2001 From: Rohit Durvasula <88731568+drohit-cb@users.noreply.github.com> Date: Wed, 7 May 2025 12:14:41 -0700 Subject: [PATCH 2/2] Releasing v0.24.0 (#420) * Add support for execution layer validator consolidations * chore: Prep for v0.24.0 release * disable shared eth staking e2e test --- CHANGELOG.md | 4 + package-lock.json | 4 +- package.json | 2 +- quickstart-template/package.json | 2 +- src/coinbase/address/external_address.ts | 23 +- src/coinbase/address/wallet_address.ts | 38 ++- src/tests/authenticator_test.ts | 4 +- src/tests/e2e.ts | 349 +++++++++++++---------- src/tests/external_address_test.ts | 85 ++---- src/tests/wallet_address_test.ts | 73 +++-- 10 files changed, 328 insertions(+), 256 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76d14985..06fa2a4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## [0.24.0] - 2025-05-06 + +- Add support for execution layer consolidations post Pectra. + ## [0.23.0] - 2025-04-16 - Add support for both consensus and execution based withdrawals post-pectra. diff --git a/package-lock.json b/package-lock.json index 911afa05..c6686182 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@coinbase/coinbase-sdk", - "version": "0.23.0", + "version": "0.24.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@coinbase/coinbase-sdk", - "version": "0.23.0", + "version": "0.24.0", "license": "ISC", "dependencies": { "@scure/bip32": "^1.4.0", diff --git a/package.json b/package.json index 9dcbf7b7..8cb847dd 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.23.0", + "version": "0.24.0", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { diff --git a/quickstart-template/package.json b/quickstart-template/package.json index d8a4a74f..4b778978 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.23.0", + "@coinbase/coinbase-sdk": "^0.24.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 dd975f0c..255072c2 100644 --- a/src/coinbase/address/external_address.ts +++ b/src/coinbase/address/external_address.ts @@ -94,6 +94,19 @@ export class ExternalAddress extends Address { return this.buildStakingOperation(amount, assetId, "claim_stake", mode, options); } + /** + * Builds a validator consolidation operation to help consolidate validators post Pectra. + * + * @param options - Additional options for the validator consolidation operation. + * + * @returns The validator consolidation operation. + */ + public async buildValidatorConsolidationOperation( + options: { [key: string]: string } = {}, + ): Promise { + return this.buildStakingOperation(0, "eth", "consolidate", StakeOptionsMode.NATIVE, options); + } + /** * Builds the staking operation based on the supplied input. * @@ -119,15 +132,7 @@ export class ExternalAddress extends Address { newOptions.mode = mode; - // 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.`); - } - - newOptions.amount = asset.toAtomicAmount(new Decimal(amount.toString())).toString(); - } + newOptions.amount = asset.toAtomicAmount(new Decimal(amount.toString())).toString(); const request = { network_id: this.getNetworkId(), diff --git a/src/coinbase/address/wallet_address.ts b/src/coinbase/address/wallet_address.ts index f56d07c1..29774ce4 100644 --- a/src/coinbase/address/wallet_address.ts +++ b/src/coinbase/address/wallet_address.ts @@ -766,6 +766,32 @@ export class WalletAddress extends Address { ); } + /** + * Creates a staking operation to consolidate. + * + * @param options - Additional options for the consolidation operation. + * + * @param timeoutSeconds - The amount to wait for the transaction to complete when broadcasted. + * @param intervalSeconds - The amount to check each time for a successful broadcast. + * + * @returns The staking operation after it's completed successfully. + */ + public async createValidatorConsolidation( + options: { [key: string]: string } = {}, + timeoutSeconds = 600, + intervalSeconds = 0.2, + ): Promise { + return this.createStakingOperation( + 0, + "eth", + "consolidate", + StakeOptionsMode.NATIVE, + options, + timeoutSeconds, + intervalSeconds, + ); + } + /** * Creates a Payload Signature. * @@ -1011,13 +1037,6 @@ export class WalletAddress extends Address { timeoutSeconds: number, intervalSeconds: number, ): Promise { - // 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."); - } - } - let stakingOperation = await this.createStakingOperationRequest( amount, assetId, @@ -1080,10 +1099,7 @@ export class WalletAddress extends Address { options.mode = mode ? mode : StakeOptionsMode.DEFAULT; - // 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(); - } + options.amount = asset.toAtomicAmount(new Decimal(amount.toString())).toString(); const stakingOperationRequest = { network_id: this.getNetworkId(), diff --git a/src/tests/authenticator_test.ts b/src/tests/authenticator_test.ts index d7d078b0..2cff282f 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.23.0,sdk_language=typescript,source=mockSource", + "sdk_version=0.24.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.23.0,sdk_language=typescript,source=mockSource", + "sdk_version=0.24.0,sdk_language=typescript,source=mockSource", ); }); }); diff --git a/src/tests/e2e.ts b/src/tests/e2e.ts index 2bb5b69a..c4390c91 100644 --- a/src/tests/e2e.ts +++ b/src/tests/e2e.ts @@ -39,118 +39,150 @@ describe("Coinbase SDK E2E Test", () => { expect(fs.existsSync("./dist/coinbase/coinbase.js")).toBe(true); }); - // CDP-266 Flaky test - // it("should be able to interact with the Coinbase SDK", async () => { - // console.log("Creating new wallet..."); - // const wallet = await Wallet.create(); - - // expect(wallet.toString()).toBeDefined(); - // expect(wallet?.getId()).toBeDefined(); - // console.log( - // `Created new wallet with ID: ${wallet.getId()}, default address: ${wallet.getDefaultAddress()}`, - // ); - - // console.log("Importing wallet with balance..."); - // const seedFile = JSON.parse(process.env.WALLET_DATA || ""); - // const walletId = Object.keys(seedFile)[0]; - // const seed = seedFile[walletId].seed; - - // const importedWallet = await Wallet.import({ - // seed, - // walletId, - // networkId: Coinbase.networks.BaseSepolia, - // }); - // expect(importedWallet).toBeDefined(); - // expect(importedWallet.getId()).toBe(walletId); - // console.log( - // `Imported wallet with ID: ${importedWallet.getId()}, default address: ${importedWallet.getDefaultAddress()}`, - // ); - // await importedWallet.saveSeedToFile("test_seed.json"); - - // try { - // const transaction = await importedWallet.faucet(); - // expect(transaction.toString()).toBeDefined(); - // } catch { - // console.log("Faucet request failed. Skipping..."); - // } - // console.log("Listing wallet addresses..."); - // const addresses = await importedWallet.listAddresses(); - // expect(addresses.length).toBeGreaterThan(0); - // console.log(`Listed addresses: ${addresses.join(", ")}`); - - // console.log("Fetching wallet balances..."); - // const balances = await importedWallet.listBalances(); - // expect(Array.from([...balances.keys()]).length).toBeGreaterThan(0); - // console.log(`Fetched balances: ${balances.toString()}`); - - // console.log("Exporting wallet..."); - // const exportedWallet = await wallet.export(); - // expect(exportedWallet.walletId).toBeDefined(); - // expect(exportedWallet.seed).toBeDefined(); - - // console.log("Saving seed to file..."); - // await wallet.saveSeedToFile("test_seed.json"); - // expect(fs.existsSync("test_seed.json")).toBe(true); - // console.log("Saved seed to test_seed.json"); - - // const unhydratedWallet = await Wallet.fetch(walletId); - // expect(unhydratedWallet.canSign()).toBe(false); - // await unhydratedWallet.loadSeedFromFile("test_seed.json"); - // expect(unhydratedWallet.canSign()).toBe(true); - // expect(unhydratedWallet.getId()).toBe(walletId); - - // console.log("Transfering 0.000000001 ETH from default address to second address..."); - // const transfer = await unhydratedWallet.createTransfer({ - // amount: 0.000000001, - // assetId: Coinbase.assets.Eth, - // destination: wallet, - // }); + /* + * CDP-266 Flaky test + * it("should be able to interact with the Coinbase SDK", async () => { + * console.log("Creating new wallet..."); + * const wallet = await Wallet.create(); + */ + + /* + * expect(wallet.toString()).toBeDefined(); + * expect(wallet?.getId()).toBeDefined(); + * console.log( + * `Created new wallet with ID: ${wallet.getId()}, default address: ${wallet.getDefaultAddress()}`, + * ); + */ + + /* + * console.log("Importing wallet with balance..."); + * const seedFile = JSON.parse(process.env.WALLET_DATA || ""); + * const walletId = Object.keys(seedFile)[0]; + * const seed = seedFile[walletId].seed; + */ + + /* + * const importedWallet = await Wallet.import({ + * seed, + * walletId, + * networkId: Coinbase.networks.BaseSepolia, + * }); + * expect(importedWallet).toBeDefined(); + * expect(importedWallet.getId()).toBe(walletId); + * console.log( + * `Imported wallet with ID: ${importedWallet.getId()}, default address: ${importedWallet.getDefaultAddress()}`, + * ); + * await importedWallet.saveSeedToFile("test_seed.json"); + */ + + /* + * try { + * const transaction = await importedWallet.faucet(); + * expect(transaction.toString()).toBeDefined(); + * } catch { + * console.log("Faucet request failed. Skipping..."); + * } + * console.log("Listing wallet addresses..."); + * const addresses = await importedWallet.listAddresses(); + * expect(addresses.length).toBeGreaterThan(0); + * console.log(`Listed addresses: ${addresses.join(", ")}`); + */ + + /* + * console.log("Fetching wallet balances..."); + * const balances = await importedWallet.listBalances(); + * expect(Array.from([...balances.keys()]).length).toBeGreaterThan(0); + * console.log(`Fetched balances: ${balances.toString()}`); + */ + + /* + * console.log("Exporting wallet..."); + * const exportedWallet = await wallet.export(); + * expect(exportedWallet.walletId).toBeDefined(); + * expect(exportedWallet.seed).toBeDefined(); + */ + + /* + * console.log("Saving seed to file..."); + * await wallet.saveSeedToFile("test_seed.json"); + * expect(fs.existsSync("test_seed.json")).toBe(true); + * console.log("Saved seed to test_seed.json"); + */ + + /* + * const unhydratedWallet = await Wallet.fetch(walletId); + * expect(unhydratedWallet.canSign()).toBe(false); + * await unhydratedWallet.loadSeedFromFile("test_seed.json"); + * expect(unhydratedWallet.canSign()).toBe(true); + * expect(unhydratedWallet.getId()).toBe(walletId); + */ + + /* + * console.log("Transfering 0.000000001 ETH from default address to second address..."); + * const transfer = await unhydratedWallet.createTransfer({ + * amount: 0.000000001, + * assetId: Coinbase.assets.Eth, + * destination: wallet, + * }); + */ // await transfer.wait(); - // expect(transfer.toString()).toBeDefined(); - // expect(await transfer.getStatus()).toBe(TransferStatus.COMPLETE); - // console.log(`Transferred 1 Gwei from ${unhydratedWallet} to ${wallet}`); - - // console.log("Fetching updated balances..."); - // const firstBalance = await unhydratedWallet.listBalances(); - // const secondBalance = await wallet.listBalances(); - // expect(firstBalance.get(Coinbase.assets.Eth)).not.toEqual("0"); - // expect(secondBalance.get(Coinbase.assets.Eth)).not.toEqual("0"); - // console.log(`First address balances: ${firstBalance}`); - // console.log(`Second address balances: ${secondBalance}`); - - // console.log("Fetching address transactions..."); - // let result; - // for (let i = 0; i < 5; i++) { - // // Try up to 5 times - // result = await (await unhydratedWallet.getDefaultAddress()).listTransactions({ limit: 1 }); - // if (result?.data.length > 0) break; - // // Wait 2 seconds between attempts - // console.log(`Waiting for transaction to be processed... (${i + 1} attempts)`); - // await new Promise(resolve => setTimeout(resolve, 2000)); - // } - // expect(result?.data.length).toBeGreaterThan(0); - - // console.log("Fetching address historical balances..."); - // const balance_result = await ( - // await unhydratedWallet.getDefaultAddress() - // ).listHistoricalBalances(Coinbase.assets.Eth, { limit: 2 }); - // expect(balance_result?.data.length).toBeGreaterThan(0); - // console.log(`First eth historical balance: ${balance_result?.data[0].amount.toString()}`); - - // const savedSeed = JSON.parse(fs.readFileSync("test_seed.json", "utf-8")); - // fs.unlinkSync("test_seed.json"); - - // expect(exportedWallet.seed.length).toBe(64); - // expect(savedSeed[exportedWallet.walletId!]).toEqual({ - // seed: exportedWallet.seed, - // encrypted: false, - // authTag: "", - // iv: "", - // networkId: exportedWallet.networkId, - // }); - // }, 60000); + /* + * expect(transfer.toString()).toBeDefined(); + * expect(await transfer.getStatus()).toBe(TransferStatus.COMPLETE); + * console.log(`Transferred 1 Gwei from ${unhydratedWallet} to ${wallet}`); + */ + + /* + * console.log("Fetching updated balances..."); + * const firstBalance = await unhydratedWallet.listBalances(); + * const secondBalance = await wallet.listBalances(); + * expect(firstBalance.get(Coinbase.assets.Eth)).not.toEqual("0"); + * expect(secondBalance.get(Coinbase.assets.Eth)).not.toEqual("0"); + * console.log(`First address balances: ${firstBalance}`); + * console.log(`Second address balances: ${secondBalance}`); + */ + + /* + * console.log("Fetching address transactions..."); + * let result; + * for (let i = 0; i < 5; i++) { + * // Try up to 5 times + * result = await (await unhydratedWallet.getDefaultAddress()).listTransactions({ limit: 1 }); + * if (result?.data.length > 0) break; + * // Wait 2 seconds between attempts + * console.log(`Waiting for transaction to be processed... (${i + 1} attempts)`); + * await new Promise(resolve => setTimeout(resolve, 2000)); + * } + * expect(result?.data.length).toBeGreaterThan(0); + */ + + /* + * console.log("Fetching address historical balances..."); + * const balance_result = await ( + * await unhydratedWallet.getDefaultAddress() + * ).listHistoricalBalances(Coinbase.assets.Eth, { limit: 2 }); + * expect(balance_result?.data.length).toBeGreaterThan(0); + * console.log(`First eth historical balance: ${balance_result?.data[0].amount.toString()}`); + */ + + /* + * const savedSeed = JSON.parse(fs.readFileSync("test_seed.json", "utf-8")); + * fs.unlinkSync("test_seed.json"); + */ + + /* + * expect(exportedWallet.seed.length).toBe(64); + * expect(savedSeed[exportedWallet.walletId!]).toEqual({ + * seed: exportedWallet.seed, + * encrypted: false, + * authTag: "", + * iv: "", + * networkId: exportedWallet.networkId, + * }); + * }, 60000); + */ it("Should be able to invoke a contract and retrieve the transaction receipt", async () => { const seedFile = JSON.parse(process.env.WALLET_DATA || ""); @@ -329,36 +361,44 @@ describe("Coinbase SDK Stake E2E Test", () => { }); }); - // CDP-266 Flaky tests - // describe("Stake: Validator Tests", () => { - // it("should list validators", async () => { - // const networkId = Coinbase.networks.EthereumMainnet; - // const assetId = Coinbase.assets.Eth; - // const status = ValidatorStatus.ACTIVE; + /* + * CDP-266 Flaky tests + * describe("Stake: Validator Tests", () => { + * it("should list validators", async () => { + * const networkId = Coinbase.networks.EthereumMainnet; + * const assetId = Coinbase.assets.Eth; + * const status = ValidatorStatus.ACTIVE; + */ // const validators = await Validator.list(networkId, assetId, status); - // expect(validators).toBeDefined(); - // expect(validators.length).toEqual(1); - // const validator = validators[0]; - // expect(validator.getStatus()).toEqual(ValidatorStatus.ACTIVE); - // expect(validator.getValidatorId()).toEqual(process.env.STAKE_VALIDATOR_ADDRESS_1 as string); - // }); - - // it("should fetch a validator", async () => { - // const networkId = Coinbase.networks.EthereumMainnet; - // const assetId = Coinbase.assets.Eth; - // const validatorId = process.env.STAKE_VALIDATOR_ADDRESS_1 as string; + /* + * expect(validators).toBeDefined(); + * expect(validators.length).toEqual(1); + * const validator = validators[0]; + * expect(validator.getStatus()).toEqual(ValidatorStatus.ACTIVE); + * expect(validator.getValidatorId()).toEqual(process.env.STAKE_VALIDATOR_ADDRESS_1 as string); + * }); + */ + + /* + * it("should fetch a validator", async () => { + * const networkId = Coinbase.networks.EthereumMainnet; + * const assetId = Coinbase.assets.Eth; + * const validatorId = process.env.STAKE_VALIDATOR_ADDRESS_1 as string; + */ // const validator = await Validator.fetch(networkId, assetId, validatorId); - // expect(validator).toBeDefined(); - // expect(validator.getStatus()).toEqual(ValidatorStatus.ACTIVE); - // expect(validator.getValidatorId()).toEqual(validatorId); - // }); - // }); + /* + * expect(validator).toBeDefined(); + * expect(validator.getStatus()).toEqual(ValidatorStatus.ACTIVE); + * expect(validator.getValidatorId()).toEqual(validatorId); + * }); + * }); + */ - describe("Stake: Context Tests", () => { + describe.skip("Stake: Context Tests", () => { it("should return stakeable balances for shared ETH staking", async () => { const address = new ExternalAddress( Coinbase.networks.EthereumMainnet, @@ -404,26 +444,35 @@ describe("Coinbase SDK Stake E2E Test", () => { expect(stakeableBalance.toNumber()).toBeGreaterThanOrEqual(0); }); - // CDP-266 Flaky test - // it("should return unstakeable balances for Dedicated ETH staking", async () => { - // // This address is expected to have 1 validator associated with it, thus returning a 32 unstake balance. - - // const address = new ExternalAddress( - // Coinbase.networks.EthereumMainnet, - // process.env.STAKE_ADDRESS_ID_2 as string, - // ); - - // const stakeableBalance = await address.unstakeableBalance( - // Coinbase.assets.Eth, - // StakeOptionsMode.NATIVE, - // ); - - // expect(stakeableBalance).toBeDefined(); - // expect(stakeableBalance.toNumber()).toBeGreaterThanOrEqual(32); - // }); + /* + * CDP-266 Flaky test + * it("should return unstakeable balances for Dedicated ETH staking", async () => { + * // This address is expected to have 1 validator associated with it, thus returning a 32 unstake balance. + */ + + /* + * const address = new ExternalAddress( + * Coinbase.networks.EthereumMainnet, + * process.env.STAKE_ADDRESS_ID_2 as string, + * ); + */ + + /* + * const stakeableBalance = await address.unstakeableBalance( + * Coinbase.assets.Eth, + * StakeOptionsMode.NATIVE, + * ); + */ + + /* + * expect(stakeableBalance).toBeDefined(); + * expect(stakeableBalance.toNumber()).toBeGreaterThanOrEqual(32); + * }); + */ }); - describe("Stake: Build Tests", () => { + // Skip until shared eth staking incident is resolved. + describe.skip("Stake: Build Tests", () => { it("should return an unsigned tx for shared ETH staking", async () => { const address = new ExternalAddress( Coinbase.networks.EthereumMainnet, diff --git a/src/tests/external_address_test.ts b/src/tests/external_address_test.ts index 2f590686..d379c2da 100644 --- a/src/tests/external_address_test.ts +++ b/src/tests/external_address_test.ts @@ -329,25 +329,6 @@ describe("ExternalAddress", () => { }); expect(Coinbase.apiClients.stake!.buildStakingOperation).toHaveBeenCalledTimes(0); }); - - it("should return an error for trying to stake less than or equal to zero", async () => { - Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL); - Coinbase.apiClients.stake!.buildStakingOperation = mockReturnValue(STAKING_OPERATION_MODEL); - - await expect( - address.buildStakeOperation(new Decimal("0"), Coinbase.assets.Eth), - ).rejects.toThrow(Error); - - expect(Coinbase.apiClients.stake!.getStakingContext).toHaveBeenCalledWith({ - address_id: address.getId(), - network_id: address.getNetworkId(), - asset_id: Coinbase.assets.Eth, - options: { - mode: StakeOptionsMode.DEFAULT, - }, - }); - expect(Coinbase.apiClients.stake!.buildStakingOperation).toHaveBeenCalledTimes(0); - }); }); describe("#buildUnstakeOperation", () => { @@ -395,25 +376,6 @@ describe("ExternalAddress", () => { expect(Coinbase.apiClients.stake!.buildStakingOperation).toHaveBeenCalledTimes(0); }); - it("should return an error for trying to unstake less than or equal to zero", async () => { - Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL); - Coinbase.apiClients.stake!.buildStakingOperation = mockReturnValue(STAKING_OPERATION_MODEL); - - await expect( - address.buildUnstakeOperation(new Decimal("0"), Coinbase.assets.Eth), - ).rejects.toThrow(Error); - - expect(Coinbase.apiClients.stake!.getStakingContext).toHaveBeenCalledWith({ - address_id: address.getId(), - network_id: address.getNetworkId(), - asset_id: Coinbase.assets.Eth, - options: { - mode: StakeOptionsMode.DEFAULT, - }, - }); - 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); @@ -441,6 +403,7 @@ describe("ExternalAddress", () => { action: "unstake", options: { mode: StakeOptionsMode.NATIVE, + amount: "0", unstake_type: "consensus", validator_pub_keys: "0x123,0x456,0x789", }, @@ -477,6 +440,7 @@ describe("ExternalAddress", () => { options: { mode: StakeOptionsMode.NATIVE, some_other_option: "value", + amount: "0", unstake_type: "consensus", validator_pub_keys: "0x123,0x456,0x789", }, @@ -509,6 +473,7 @@ describe("ExternalAddress", () => { action: "unstake", options: { mode: StakeOptionsMode.NATIVE, + amount: "0", unstake_type: "execution", validator_unstake_amounts: '{"0x123":"1000000000000000000000","0x456":"2000000000000000000000"}', @@ -542,6 +507,7 @@ describe("ExternalAddress", () => { options: { mode: StakeOptionsMode.NATIVE, some_other_option: "value", + amount: "0", unstake_type: "execution", validator_unstake_amounts: '{"0x123":"1000000000000000000000"}', }, @@ -596,25 +562,6 @@ describe("ExternalAddress", () => { expect(Coinbase.apiClients.stake!.buildStakingOperation).toHaveBeenCalledTimes(0); }); - it("should return an error for trying to claim stake less than or equal to zero", async () => { - Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL); - Coinbase.apiClients.stake!.buildStakingOperation = mockReturnValue(STAKING_OPERATION_MODEL); - - await expect( - address.buildClaimStakeOperation(new Decimal("0"), Coinbase.assets.Eth), - ).rejects.toThrow(Error); - - expect(Coinbase.apiClients.stake!.getStakingContext).toHaveBeenCalledWith({ - address_id: address.getId(), - network_id: address.getNetworkId(), - asset_id: Coinbase.assets.Eth, - options: { - mode: StakeOptionsMode.DEFAULT, - }, - }); - expect(Coinbase.apiClients.stake!.buildStakingOperation).toHaveBeenCalledTimes(0); - }); - it("should return an error for trying to claim stake for native eth", async () => { await expect( address.buildClaimStakeOperation( @@ -626,6 +573,30 @@ describe("ExternalAddress", () => { }); }); + describe("#buildValidatorConsolidationOperation", () => { + it("should successfully build a validator consolidation operation", async () => { + const mockResponse = { data: "mockStakingOperationResponse" }; + Coinbase.apiClients.stake!.buildStakingOperation = jest.fn().mockResolvedValue(mockResponse); + + const options = { someOption: "value" }; + const op = await address.buildValidatorConsolidationOperation(options); + + expect(Coinbase.apiClients.stake!.buildStakingOperation).toHaveBeenCalledWith({ + network_id: address.getNetworkId(), + asset_id: "eth", + address_id: address.getId(), + action: "consolidate", + options: { + someOption: "value", + amount: "0", + mode: StakeOptionsMode.NATIVE, + }, + }); + + expect(op).toBeInstanceOf(StakingOperation); + }); + }); + describe("#stakingRewards", () => { it("should return staking rewards successfully", async () => { Coinbase.apiClients.stake!.fetchStakingRewards = mockReturnValue(STAKING_REWARD_RESPONSE); diff --git a/src/tests/wallet_address_test.ts b/src/tests/wallet_address_test.ts index a549b2ad..1cb8ebb6 100644 --- a/src/tests/wallet_address_test.ts +++ b/src/tests/wallet_address_test.ts @@ -608,32 +608,23 @@ describe("WalletAddress", () => { ); 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); - Coinbase.apiClients.walletStake!.createStakingOperation = - mockReturnValue(STAKING_OPERATION_MODEL); - Coinbase.apiClients.walletStake!.broadcastStakingOperation = - mockReturnValue(STAKING_OPERATION_MODEL); - STAKING_OPERATION_MODEL.status = StakingOperationStatusEnum.Failed; - Coinbase.apiClients.walletStake!.getStakingOperation = - mockReturnValue(STAKING_OPERATION_MODEL); - - const op = await walletAddress.createStake(0.001, Coinbase.assets.Eth); - expect(op).toBeInstanceOf(StakingOperation); - expect(op.getStatus()).toEqual(StakingOperationStatusEnum.Failed); - }); + 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); + Coinbase.apiClients.walletStake!.createStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + Coinbase.apiClients.walletStake!.broadcastStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + STAKING_OPERATION_MODEL.status = StakingOperationStatusEnum.Failed; + Coinbase.apiClients.walletStake!.getStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); - it("should not create a staking operation from the address with zero amount", async () => { - Coinbase.apiClients.asset!.getAsset = getAssetMock(); - Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL); + const op = await walletAddress.createStake(0.001, Coinbase.assets.Eth); - await expect( - async () => await walletAddress.createStake(0.0, Coinbase.assets.Eth), - ).rejects.toThrow(Error); + expect(op).toBeInstanceOf(StakingOperation); + expect(op.getStatus()).toEqual(StakingOperationStatusEnum.Failed); + }); }); it("should create a staking operation from the address when broadcast returns empty transactions", async () => { @@ -706,6 +697,7 @@ describe("WalletAddress", () => { action: "unstake", options: { mode: StakeOptionsMode.NATIVE, + amount: "1000000000000000", // We let this extraneous amount to be passed through since it isn't used in the backend. unstake_type: "consensus", validator_pub_keys: "0x123,0x456,0x789", }, @@ -748,6 +740,7 @@ describe("WalletAddress", () => { options: { mode: StakeOptionsMode.NATIVE, unstake_type: "execution", + amount: "1000000000000000", // We let this extraneous amount to be passed through since it isn't used in the backend. validator_unstake_amounts: '{"0x123":"100000000000000000000","0x456":"200000000000000000000"}', }, @@ -776,6 +769,40 @@ describe("WalletAddress", () => { }); }); + describe("#createValidatorConsolidation", () => { + it("should successfully create a validator consolidation operation", async () => { + Coinbase.apiClients.walletStake!.createStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + Coinbase.apiClients.walletStake!.broadcastStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + Coinbase.apiClients.walletStake!.getStakingOperation = + mockReturnValue(STAKING_OPERATION_MODEL); + + const op = await walletAddress.createValidatorConsolidation({ + source_validator_pubkey: "0xabc123", + target_validator_pubkey: "0xdef456", + }); + + expect(Coinbase.apiClients.walletStake!.createStakingOperation).toHaveBeenCalledWith( + walletAddress.getWalletId(), + walletAddress.getId(), + { + network_id: walletAddress.getNetworkId(), + asset_id: "eth", + action: "consolidate", + options: { + mode: "native", + amount: "0", + source_validator_pubkey: "0xabc123", + target_validator_pubkey: "0xdef456", + }, + }, + ); + + expect(op).toBeInstanceOf(StakingOperation); + }); + }); + describe("#stakeableBalance", () => { it("should return the stakeable balance successfully with default params", async () => { Coinbase.apiClients.stake!.getStakingContext = mockReturnValue(STAKING_CONTEXT_MODEL);