From a470ce07a908c4f587176dde0fc2d66540f83379 Mon Sep 17 00:00:00 2001 From: arc <224894192+arc0btc@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:06:28 -0600 Subject: [PATCH] fix(stacking): add pox-5 support for Epoch 4.0 hard fork stacks-core 4.0.1 activates pox-5 in place of pox-4. StackingService hardcoded pox-4 for stack-stx/stack-extend/stack-increase/delegate-stx/ revoke-delegate-stx/get-stacker-info, which will fail post-activation. Add a POX_5 contract constant (same boot-contract address as pox-4, following the pox-2/3/4 pattern) and resolve the active PoX contract dynamically at call time via the node's /v2/pox contract_id, falling back to the static POX_5 address if that lookup fails. This avoids another hardcoded-version cliff on the next PoX bump. --- src/lib/config/contracts.ts | 7 +++++ src/lib/services/stacking.service.ts | 40 +++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/lib/config/contracts.ts b/src/lib/config/contracts.ts index dee56693..513aad42 100644 --- a/src/lib/config/contracts.ts +++ b/src/lib/config/contracts.ts @@ -17,6 +17,12 @@ export const MAINNET_CONTRACTS = { // Stacking POX_4: "SP000000000000000000002Q6VF78.pox-4", + // Epoch 4.0 hard fork (stacks-core 4.0.1): pox-5 replaces pox-4 at the same + // boot-contract address, following the pox-2/pox-3/pox-4 deployment pattern. + // Prefer resolving the active PoX contract dynamically via + // StackingService.getActivePoxContract() (reads /v2/pox contract_id); + // this is the static fallback if that lookup fails. + POX_5: "SP000000000000000000002Q6VF78.pox-5", // ALEX DEX (SDK handles most operations, but we need pool contract for queries) ALEX_AMM_POOL: "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.amm-swap-pool-v1-1", @@ -167,6 +173,7 @@ export const TESTNET_CONTRACTS = { // Stacking POX_4: "ST000000000000000000002AMW42H.pox-4", + POX_5: "ST000000000000000000002AMW42H.pox-5", // ERC-8004 Identity & Reputation IDENTITY_REGISTRY: "ST3YT0XW92E6T2FE59B2G5N2WNNFSBZ6MZKQS5D18.identity-registry-v2", diff --git a/src/lib/services/stacking.service.ts b/src/lib/services/stacking.service.ts index beadf8b0..0aaefc6e 100644 --- a/src/lib/services/stacking.service.ts +++ b/src/lib/services/stacking.service.ts @@ -38,6 +38,24 @@ export class StackingService { return this.hiro.getPoxInfo(); } + /** + * Resolve the currently active PoX contract (e.g. pox-4, pox-5) from the + * node's /v2/pox response, instead of hardcoding a version. Falls back to + * the static POX_5 contract if the lookup fails, so stacking keeps working + * across future PoX version bumps without a code change. + */ + private async getActivePoxContract(): Promise { + try { + const poxInfo = await this.hiro.getPoxInfo(); + if (poxInfo?.contract_id) { + return poxInfo.contract_id; + } + } catch { + // Fall through to static fallback + } + return this.contracts.POX_5; + } + /** * Get stacking status for an address * Note: Returns whether the address is stacking, but detailed amounts require proper CV parsing @@ -45,7 +63,7 @@ export class StackingService { async getStackingStatus(address: string): Promise { try { const result = await this.hiro.callReadOnlyFunction( - this.contracts.POX_4, + await this.getActivePoxContract(), "get-stacker-info", [{ type: "principal", value: address } as unknown as ClarityValue], address @@ -87,7 +105,9 @@ export class StackingService { startBurnHeight: number, lockPeriod: number ): Promise { - const { address: contractAddress, name: contractName } = parseContractId(this.contracts.POX_4); + const { address: contractAddress, name: contractName } = parseContractId( + await this.getActivePoxContract() + ); const functionArgs: ClarityValue[] = [ uintCV(amount), @@ -123,7 +143,9 @@ export class StackingService { extendCount: number, poxAddress: { version: number; hashbytes: string } ): Promise { - const { address: contractAddress, name: contractName } = parseContractId(this.contracts.POX_4); + const { address: contractAddress, name: contractName } = parseContractId( + await this.getActivePoxContract() + ); const functionArgs: ClarityValue[] = [ uintCV(extendCount), @@ -150,7 +172,9 @@ export class StackingService { account: Account, increaseAmount: bigint ): Promise { - const { address: contractAddress, name: contractName } = parseContractId(this.contracts.POX_4); + const { address: contractAddress, name: contractName } = parseContractId( + await this.getActivePoxContract() + ); const functionArgs: ClarityValue[] = [uintCV(increaseAmount)]; @@ -180,7 +204,9 @@ export class StackingService { untilBurnHeight?: number, poxAddress?: { version: number; hashbytes: string } ): Promise { - const { address: contractAddress, name: contractName } = parseContractId(this.contracts.POX_4); + const { address: contractAddress, name: contractName } = parseContractId( + await this.getActivePoxContract() + ); const functionArgs: ClarityValue[] = [ uintCV(amount), @@ -208,7 +234,9 @@ export class StackingService { * Revoke delegation */ async revokeDelegation(account: Account): Promise { - const { address: contractAddress, name: contractName } = parseContractId(this.contracts.POX_4); + const { address: contractAddress, name: contractName } = parseContractId( + await this.getActivePoxContract() + ); // No assets moved from sender (revokes delegation permission) return callContract(account, {