@@ -171,17 +171,15 @@ describe('Stacks: Contract Builder', function () {
171171 it ( 'an unsigned caller-allowlisted non-canonical pox-5 (dummy) contract call transaction' , async ( ) => {
172172 // The caller supplies the staking contract it is configured for — no SDK change needed.
173173 const builder = factoryProd . getContractBuilder ( ) ;
174- builder . allowedStakingContracts ( [
175- { name : 'pox-5-dummy-v1' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ,
176- ] ) ;
174+ builder . allowedStakingContracts ( [ { name : 'pox-5-test' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ] ) ;
177175 builder . fee ( { fee : '180' } ) ;
178176 builder . nonce ( 0 ) ;
179177 builder . contractAddress ( 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' ) ;
180- builder . contractName ( 'pox-5-dummy-v1 ' ) ;
178+ builder . contractName ( 'pox-5-test ' ) ;
181179 builder . functionName ( 'register-for-bond' ) ;
182180 builder . functionArgs ( [
183181 { type : 'uint128' , val : '210' } ,
184- { type : 'principal' , val : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY.signer-manager-stackslabs-dummy-v1 ' } ,
182+ { type : 'principal' , val : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY.signer-manager-stackslabs-test ' } ,
185183 { type : 'uint128' , val : '1005000' } ,
186184 { type : 'optional' } ,
187185 { type : 'optional' } ,
@@ -191,25 +189,25 @@ describe('Stacks: Contract Builder', function () {
191189
192190 const tx = await builder . build ( ) ;
193191 const txJson = tx . toJson ( ) ;
194- should . deepEqual ( txJson . payload . contractName , 'pox-5-dummy-v1 ' ) ;
192+ should . deepEqual ( txJson . payload . contractName , 'pox-5-test ' ) ;
195193 should . deepEqual ( txJson . payload . contractAddress , 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' ) ;
196194 should . deepEqual ( txJson . payload . functionName , 'register-for-bond' ) ;
197195 } ) ;
198196
199197 it ( 'rehydrates an allowlisted non-canonical contract call (sign / explain / fee-rebuild path)' , async ( ) => {
200198 // `signTransaction` / `explainTransaction` / the fee re-build all go through `factory.from(hex)`,
201199 // which re-creates the builder — re-parsing must not reject the allowlisted contract.
202- const allowlist = [ { name : 'pox-5-dummy-v1 ' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ] ;
200+ const allowlist = [ { name : 'pox-5-test ' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ] ;
203201 const builder = factoryProd . getContractBuilder ( ) ;
204202 builder . allowedStakingContracts ( allowlist ) ;
205203 builder . fee ( { fee : '180' } ) ;
206204 builder . nonce ( 0 ) ;
207205 builder . contractAddress ( 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' ) ;
208- builder . contractName ( 'pox-5-dummy-v1 ' ) ;
206+ builder . contractName ( 'pox-5-test ' ) ;
209207 builder . functionName ( 'register-for-bond' ) ;
210208 builder . functionArgs ( [
211209 { type : 'uint128' , val : '210' } ,
212- { type : 'principal' , val : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY.signer-manager-stackslabs-dummy-v1 ' } ,
210+ { type : 'principal' , val : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY.signer-manager-stackslabs-test ' } ,
213211 { type : 'uint128' , val : '1005000' } ,
214212 { type : 'optional' } ,
215213 { type : 'optional' } ,
@@ -223,55 +221,47 @@ describe('Stacks: Contract Builder', function () {
223221 reparsed . numberSignatures ( 1 ) ;
224222 const payload = ( await reparsed . build ( ) ) . toJson ( ) . payload ;
225223 should . deepEqual ( payload . contractAddress , 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' ) ;
226- should . deepEqual ( payload . contractName , 'pox-5-dummy-v1 ' ) ;
224+ should . deepEqual ( payload . contractName , 'pox-5-test ' ) ;
227225
228226 // Without the allowlist the rebuild is rejected — canonical validation is preserved (no relaxation).
229227 ( ( ) => factoryProd . from ( tx . toBroadcastFormat ( ) ) ) . should . throw ( ParseTransactionError ) ;
230228 } ) ;
231229
232230 it ( 'rejects an allowlisted name followed by a canonical address (order-independent pair check)' , function ( ) {
233231 const builder = factoryProd . getContractBuilder ( ) ;
234- builder . allowedStakingContracts ( [
235- { name : 'pox-5-dummy-v1' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ,
236- ] ) ;
237- builder . contractName ( 'pox-5-dummy-v1' ) ; // allowlisted name first
232+ builder . allowedStakingContracts ( [ { name : 'pox-5-test' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ] ) ;
233+ builder . contractName ( 'pox-5-test' ) ; // allowlisted name first
238234 ( ( ) => builder . contractAddress ( 'SP000000000000000000002Q6VF78' ) ) . should . throw ( / n o t a n a l l o w l i s t e d p a i r / ) ;
239235 } ) ;
240236
241237 it ( 'rejects an allowlisted address followed by a canonical name (order-independent pair check)' , function ( ) {
242238 const builder = factoryProd . getContractBuilder ( ) ;
243- builder . allowedStakingContracts ( [
244- { name : 'pox-5-dummy-v1' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ,
245- ] ) ;
239+ builder . allowedStakingContracts ( [ { name : 'pox-5-test' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ] ) ;
246240 builder . contractAddress ( 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' ) ; // allowlisted address first
247- ( ( ) => builder . contractName ( 'pox-5 ' ) ) . should . throw ( / n o t a n a l l o w l i s t e d p a i r / ) ;
241+ ( ( ) => builder . contractName ( 'pox-4 ' ) ) . should . throw ( / n o t a n a l l o w l i s t e d p a i r / ) ;
248242 } ) ;
249243
250244 it ( 'rejects duplicate allowlisted contract names' , function ( ) {
251245 const builder = factoryProd . getContractBuilder ( ) ;
252246 ( ( ) =>
253247 builder . allowedStakingContracts ( [
254- { name : 'pox-5-dummy-v1 ' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ,
255- { name : 'pox-5-dummy-v1 ' , address : 'SP000000000000000000002Q6VF78' } ,
248+ { name : 'pox-5-test ' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ,
249+ { name : 'pox-5-test ' , address : 'SP000000000000000000002Q6VF78' } ,
256250 ] ) ) . should . throw ( / D u p l i c a t e s t a k i n g c o n t r a c t n a m e / ) ;
257251 } ) ;
258252
259253 it ( 'rejects an allowlisted name paired with a different address' , function ( ) {
260254 const builder = factoryProd . getContractBuilder ( ) ;
261- builder . allowedStakingContracts ( [
262- { name : 'pox-5-dummy-v1' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ,
263- ] ) ;
255+ builder . allowedStakingContracts ( [ { name : 'pox-5-test' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ] ) ;
264256 builder . contractAddress ( 'SP000000000000000000002Q6VF78' ) ; // canonical address, wrong pair
265- ( ( ) => builder . contractName ( 'pox-5-dummy-v1 ' ) ) . should . throw ( / n o t a n a l l o w l i s t e d p a i r / ) ;
257+ ( ( ) => builder . contractName ( 'pox-5-test ' ) ) . should . throw ( / n o t a n a l l o w l i s t e d p a i r / ) ;
266258 } ) ;
267259
268260 it ( 'rejects a function that is not in the pox-5 set for an allowlisted contract' , function ( ) {
269261 const builder = factoryProd . getContractBuilder ( ) ;
270- builder . allowedStakingContracts ( [
271- { name : 'pox-5-dummy-v1' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ,
272- ] ) ;
262+ builder . allowedStakingContracts ( [ { name : 'pox-5-test' , address : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' } ] ) ;
273263 builder . contractAddress ( 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' ) ;
274- builder . contractName ( 'pox-5-dummy-v1 ' ) ;
264+ builder . contractName ( 'pox-5-test ' ) ;
275265 // `stack-stx` is a pox-4 function, not part of the pox-5 ABI the dummy mirrors.
276266 ( ( ) => builder . functionName ( 'stack-stx' ) ) . should . throw ( / i s n o t s u p p o r t e d c o n t r a c t f u n c t i o n n a m e / ) ;
277267 } ) ;
@@ -288,6 +278,133 @@ describe('Stacks: Contract Builder', function () {
288278 ) ;
289279 } ) ;
290280
281+ describe ( 'network-configured staking contract pairs (SI-2089)' , function ( ) {
282+ // The mainnet Stx network declares this as a pair-scoped, self-expiring pox-5 allowance.
283+ const CONFIGURED_ADDRESS = 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY' ;
284+ const CANONICAL_ADDRESS = 'SP000000000000000000002Q6VF78' ;
285+
286+ const withPairs = ( pairs : ReadonlyArray < { name : string ; address : string ; expiresAt ?: string } > ) =>
287+ ( {
288+ ...coins . get ( coinName ) ,
289+ network : { ...coins . get ( coinName ) . network , allowedStakingContracts : pairs } ,
290+ } as any ) ;
291+
292+ const buildRegisterForBond = ( factoryInstance : StxLib . TransactionBuilderFactory ) => {
293+ const builder = factoryInstance . getContractBuilder ( ) ;
294+ builder . fee ( { fee : '180' } ) ;
295+ builder . nonce ( 0 ) ;
296+ builder . contractAddress ( CONFIGURED_ADDRESS ) ;
297+ builder . contractName ( 'pox-5' ) ;
298+ builder . functionName ( 'register-for-bond' ) ;
299+ builder . functionArgs ( [
300+ { type : 'uint128' , val : '210' } ,
301+ { type : 'principal' , val : 'SP3JVSE4ZB5GMDJP5XBPX7ZB1BG3QDFW7RCEQJDMY.signer-manager' } ,
302+ { type : 'uint128' , val : '1005000' } ,
303+ { type : 'optional' } ,
304+ { type : 'optional' } ,
305+ ] ) ;
306+ builder . fromPubKey ( testData . TX_SENDER . pub ) ;
307+ builder . numberSignatures ( 1 ) ;
308+ return builder ;
309+ } ;
310+
311+ it ( 'accepts pox-5 at the configured network address' , async function ( ) {
312+ const builder = buildRegisterForBond ( factoryProd ) ;
313+ const tx = await builder . build ( ) ;
314+ should . deepEqual ( tx . toJson ( ) . payload . contractAddress , CONFIGURED_ADDRESS ) ;
315+ should . deepEqual ( tx . toJson ( ) . payload . contractName , 'pox-5' ) ;
316+ } ) ;
317+
318+ it ( 'parses a configured-address register-for-bond via from(hex) with NO caller allowlist' , async function ( ) {
319+ const tx = await buildRegisterForBond ( factoryProd ) . build ( ) ;
320+ const reparsed = factoryProd . from ( tx . toBroadcastFormat ( ) ) ;
321+ reparsed . fromPubKey ( testData . TX_SENDER . pub ) ;
322+ reparsed . numberSignatures ( 1 ) ;
323+ const payload = ( await reparsed . build ( ) ) . toJson ( ) . payload as any ;
324+ should . deepEqual ( payload . contractAddress , CONFIGURED_ADDRESS ) ;
325+ should . deepEqual ( payload . contractName , 'pox-5' ) ;
326+ } ) ;
327+
328+ it ( 'rejects pox-4 at the configured network address (reviewer regression)' , function ( ) {
329+ const builder = factoryProd . getContractBuilder ( ) ;
330+ builder . contractName ( 'pox-4' ) ;
331+ ( ( ) => builder . contractAddress ( CONFIGURED_ADDRESS ) ) . should . throw ( / n o t a n a l l o w l i s t e d p a i r / ) ;
332+ } ) ;
333+
334+ it ( 'rejects send-many-memo at the configured network address' , function ( ) {
335+ const builder = factoryProd . getContractBuilder ( ) ;
336+ builder . contractName ( 'send-many-memo' ) ;
337+ ( ( ) => builder . contractAddress ( CONFIGURED_ADDRESS ) ) . should . throw ( / n o t a n a l l o w l i s t e d p a i r / ) ;
338+ } ) ;
339+
340+ it ( 'rejects an unknown address' , function ( ) {
341+ const builder = factoryProd . getContractBuilder ( ) ;
342+ ( ( ) => builder . contractAddress ( 'SP2TYC2AVC4GSB01F78H0XSVTH8WRGV95BXFG4BBR' ) ) . should . throw (
343+ / I n v a l i d c o n t r a c t a d d r e s s /
344+ ) ;
345+ } ) ;
346+
347+ it ( 'still accepts canonical pox-5 and pox-4 at the canonical address' , function ( ) {
348+ const pox5 = factoryProd . getContractBuilder ( ) ;
349+ pox5 . contractName ( 'pox-5' ) ;
350+ ( ( ) => pox5 . contractAddress ( CANONICAL_ADDRESS ) ) . should . not . throw ( ) ;
351+ const pox4 = factoryProd . getContractBuilder ( ) ;
352+ pox4 . contractName ( 'pox-4' ) ;
353+ ( ( ) => pox4 . contractAddress ( CANONICAL_ADDRESS ) ) . should . not . throw ( ) ;
354+ } ) ;
355+
356+ it ( 'honours the expiry: expired entry rejected, non-expired and absent accepted' , function ( ) {
357+ const expired = new StxLib . TransactionBuilderFactory (
358+ withPairs ( [ { name : 'pox-5' , address : CONFIGURED_ADDRESS , expiresAt : '2020-01-01' } ] )
359+ ) ;
360+ const expiredBuilder = expired . getContractBuilder ( ) ;
361+ expiredBuilder . contractName ( 'pox-5' ) ;
362+ ( ( ) => expiredBuilder . contractAddress ( CONFIGURED_ADDRESS ) ) . should . throw ( / I n v a l i d c o n t r a c t a d d r e s s / ) ;
363+
364+ const active = new StxLib . TransactionBuilderFactory (
365+ withPairs ( [ { name : 'pox-5' , address : CONFIGURED_ADDRESS , expiresAt : '2999-01-01' } ] )
366+ ) ;
367+ const activeBuilder = active . getContractBuilder ( ) ;
368+ activeBuilder . contractName ( 'pox-5' ) ;
369+ ( ( ) => activeBuilder . contractAddress ( CONFIGURED_ADDRESS ) ) . should . not . throw ( ) ;
370+
371+ const noExpiry = new StxLib . TransactionBuilderFactory (
372+ withPairs ( [ { name : 'pox-5' , address : CONFIGURED_ADDRESS } ] )
373+ ) ;
374+ const noExpiryBuilder = noExpiry . getContractBuilder ( ) ;
375+ noExpiryBuilder . contractName ( 'pox-5' ) ;
376+ ( ( ) => noExpiryBuilder . contractAddress ( CONFIGURED_ADDRESS ) ) . should . not . throw ( ) ;
377+ } ) ;
378+
379+ it ( 'keeps the caller-supplied allowlist as a working override' , async function ( ) {
380+ // A caller-only pair (not declared on the network) still works when passed explicitly.
381+ const DUMMY_ADDRESS = 'SP2T758K6T2YRKG9Q0TJ16B6FP5QQREWZSESRS0PY' ;
382+ const builder = factoryProd . getContractBuilder ( ) ;
383+ builder . allowedStakingContracts ( [ { name : 'pox-5-test' , address : DUMMY_ADDRESS } ] ) ;
384+ builder . fee ( { fee : '180' } ) ;
385+ builder . nonce ( 0 ) ;
386+ builder . contractAddress ( DUMMY_ADDRESS ) ;
387+ builder . contractName ( 'pox-5-test' ) ;
388+ builder . functionName ( 'register-for-bond' ) ;
389+ builder . functionArgs ( [
390+ { type : 'uint128' , val : '210' } ,
391+ { type : 'principal' , val : `${ DUMMY_ADDRESS } .signer-manager` } ,
392+ { type : 'uint128' , val : '1005000' } ,
393+ { type : 'optional' } ,
394+ { type : 'optional' } ,
395+ ] ) ;
396+ builder . fromPubKey ( testData . TX_SENDER . pub ) ;
397+ builder . numberSignatures ( 1 ) ;
398+ const tx = await builder . build ( ) ;
399+ should . deepEqual ( tx . toJson ( ) . payload . contractAddress , DUMMY_ADDRESS ) ;
400+
401+ // Without the caller allowlist the same address is not accepted by the network.
402+ ( ( ) => factoryProd . getContractBuilder ( ) . contractAddress ( DUMMY_ADDRESS ) ) . should . throw (
403+ / I n v a l i d c o n t r a c t a d d r e s s /
404+ ) ;
405+ } ) ;
406+ } ) ;
407+
291408 it ( 'a signed contract call with args' , async ( ) => {
292409 const builder = initTxBuilder ( ) ;
293410 builder . functionArgs ( [
0 commit comments