From 832c90cd3e999b2492299abee282ace0dad3b0f9 Mon Sep 17 00:00:00 2001 From: arc <224894192+arc0btc@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:37:54 -0600 Subject: [PATCH] feat(stacking): resolve PoX contract dynamically ahead of pox-5 activation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stacks-core 4.0.0 activates Epoch 4.0 / PoX-5 at Bitcoin height ~960230 (~2026-07-29). stacking.service.ts hardcoded pox-4 for every stack/ delegate/extend/increase/revoke call, which would silently target a deprecated contract post-activation. StackingService now resolves the active PoX contract at call time from the node's /v2/pox `contract_id` (falling back to the static POX_4 constant only if that call fails), so it survives this and future PoX version transitions without a code change. Added POX_5 to contracts.ts for reference/fallback use. No other skill (stackspot, stacking-delegation, dual-stacking, stacking-lottery) hardcodes a pox contract or calls the removed pox-5 functions (allow-contract-caller, disallow-contract-caller, check-caller-allowed) — audited via repo-wide grep, no changes needed there. --- src/lib/config/contracts.ts | 9 +++++++- src/lib/services/stacking.service.ts | 31 ++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/lib/config/contracts.ts b/src/lib/config/contracts.ts index dee56693..c0e136a1 100644 --- a/src/lib/config/contracts.ts +++ b/src/lib/config/contracts.ts @@ -16,7 +16,13 @@ export const MAINNET_CONTRACTS = { BNS: "SP000000000000000000002Q6VF78.bns", // Stacking + // NOTE: pox-4 is superseded by pox-5 at Epoch 4.0 activation (stacks-core 4.0.0, + // Bitcoin height ~960230, ~2026-07-29). Existing pox-4 locks are unaffected, but new + // stack/delegate/extend/increase calls must target the currently active PoX contract. + // stacking.service.ts resolves this dynamically via /v2/pox `contract_id`; POX_4/POX_5 + // here are fallbacks only — do not hardcode either into new call sites. POX_4: "SP000000000000000000002Q6VF78.pox-4", + 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", @@ -165,8 +171,9 @@ export const TESTNET_CONTRACTS = { // BNS BNS: "ST000000000000000000002AMW42H.bns", - // Stacking + // Stacking (see mainnet note above re: pox-4 -> pox-5) 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..5a49ec84 100644 --- a/src/lib/services/stacking.service.ts +++ b/src/lib/services/stacking.service.ts @@ -38,14 +38,33 @@ export class StackingService { return this.hiro.getPoxInfo(); } + /** + * Resolve the currently active PoX contract from the node (/v2/pox `contract_id`) + * rather than a hardcoded version. This is what makes stack/delegate/extend/increase + * calls survive PoX version transitions (e.g. pox-4 -> pox-5 at Epoch 4.0) without a + * code change. Falls back to the last-known POX_4 constant if the node call fails. + */ + 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_4; + } + /** * Get stacking status for an address * Note: Returns whether the address is stacking, but detailed amounts require proper CV parsing */ async getStackingStatus(address: string): Promise { try { + const poxContract = await this.getActivePoxContract(); const result = await this.hiro.callReadOnlyFunction( - this.contracts.POX_4, + poxContract, "get-stacker-info", [{ type: "principal", value: address } as unknown as ClarityValue], address @@ -87,7 +106,7 @@ 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 +142,7 @@ 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 +169,7 @@ 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 +199,7 @@ 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 +227,7 @@ 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, {