1- import { connect , isConnected , request } from "https://esm.sh/@stacks/connect?bundle&target=es2020" ;
1+ import {
2+ connect ,
3+ disconnect ,
4+ isConnected ,
5+ request ,
6+ } from "https://esm.sh/@stacks/connect?bundle&target=es2020" ;
7+ import { createNetwork } from "https://esm.sh/@stacks/network@7.2.0?bundle&target=es2020" ;
28import {
39 Cl ,
410 Pc ,
511 cvToJSON ,
6- deserializeCV ,
12+ fetchCallReadOnlyFunction ,
713 principalCV ,
814 serializeCV ,
915} from "https://esm.sh/@stacks/transactions@7.2.0?bundle&target=es2020" ;
@@ -86,6 +92,7 @@ const elements = {
8692 transferValidAfter : document . getElementById ( "transfer-valid-after" ) ,
8793 walletStatus : document . getElementById ( "wallet-status" ) ,
8894 connectWallet : document . getElementById ( "connect-wallet" ) ,
95+ disconnectWallet : document . getElementById ( "disconnect-wallet" ) ,
8996 getPipe : document . getElementById ( "get-pipe" ) ,
9097 openPipe : document . getElementById ( "open-pipe" ) ,
9198 forceCancel : document . getElementById ( "force-cancel" ) ,
@@ -108,7 +115,18 @@ function normalizedText(value) {
108115}
109116
110117function isStacksAddress ( value ) {
111- return / ^ S [ P M T ] [ A - Z 0 - 9 ] { 38 , 42 } $ / i. test ( normalizedText ( value ) ) ;
118+ return / ^ S [ P M T N ] [ A - Z 0 - 9 ] { 38 , 42 } $ / i. test ( normalizedText ( value ) ) ;
119+ }
120+
121+ function isAddressOnNetwork ( address , network = readNetwork ( ) ) {
122+ const text = normalizedText ( address ) . toUpperCase ( ) ;
123+ if ( ! text ) {
124+ return false ;
125+ }
126+ if ( network === "mainnet" ) {
127+ return / ^ S [ P M ] / . test ( text ) ;
128+ }
129+ return / ^ S [ T N ] / . test ( text ) ;
112130}
113131
114132function isPrincipalText ( value ) {
@@ -219,15 +237,6 @@ function unwrapClarityJson(value) {
219237 return output ;
220238}
221239
222- function decodeReadOnlyResult ( resultHex ) {
223- const hex = normalizedText ( resultHex ) ;
224- if ( ! hex ) {
225- return null ;
226- }
227- const decoded = deserializeCV ( hex ) ;
228- return unwrapClarityJson ( cvToJSON ( decoded ) ) ;
229- }
230-
231240function parseContractId ( ) {
232241 const raw = normalizedText ( elements . contractId . value ) ;
233242 if ( ! raw . includes ( "." ) ) {
@@ -431,33 +440,35 @@ function parseValidAfterCV() {
431440 return { cv : Cl . some ( Cl . uint ( value ) ) , text : value . toString ( 10 ) } ;
432441}
433442
434- function extractAddress ( response ) {
443+ function extractAddress ( response , network = readNetwork ( ) ) {
435444 const seen = new Set ( ) ;
445+ const found = [ ] ;
436446 const crawl = ( value ) => {
437- if ( value == null ) return null ;
438- if ( typeof value === "string" && isStacksAddress ( value ) ) return value ;
439- if ( typeof value !== "object" ) return null ;
440- if ( seen . has ( value ) ) return null ;
447+ if ( value == null ) return ;
448+ if ( typeof value === "string" && isStacksAddress ( value ) ) {
449+ found . push ( value ) ;
450+ return ;
451+ }
452+ if ( typeof value !== "object" ) return ;
453+ if ( seen . has ( value ) ) return ;
441454 seen . add ( value ) ;
442455
443456 if ( Array . isArray ( value ) ) {
444457 for ( const entry of value ) {
445- const found = crawl ( entry ) ;
446- if ( found ) return found ;
458+ crawl ( entry ) ;
447459 }
448- return null ;
460+ return ;
449461 }
450462
451463 if ( typeof value . address === "string" && isStacksAddress ( value . address ) ) {
452- return value . address ;
464+ found . push ( value . address ) ;
453465 }
454466 for ( const nested of Object . values ( value ) ) {
455- const found = crawl ( nested ) ;
456- if ( found ) return found ;
467+ crawl ( nested ) ;
457468 }
458- return null ;
459469 } ;
460- return crawl ( response ) ;
470+ crawl ( response ) ;
471+ return found . find ( ( address ) => isAddressOnNetwork ( address , network ) ) || found [ 0 ] || null ;
461472}
462473
463474function extractSignature ( response ) {
@@ -495,7 +506,7 @@ async function ensureWallet({ interactive }) {
495506
496507 if ( connected || interactive ) {
497508 const addresses = await request ( "getAddresses" ) ;
498- const address = extractAddress ( addresses ) ;
509+ const address = extractAddress ( addresses , readNetwork ( ) ) ;
499510 if ( ! address ) {
500511 throw new Error ( "No Stacks address was returned by the wallet" ) ;
501512 }
@@ -581,59 +592,80 @@ async function handleConnectWallet() {
581592 }
582593}
583594
584- function toClarityPrincipalLiteral ( principal ) {
585- return `'${ normalizedText ( principal ) } ` ;
586- }
595+ async function handleDisconnectWallet ( ) {
596+ const previousAddress = state . connectedAddress ;
597+ try {
598+ await disconnect ( ) ;
599+ } catch {
600+ // Some providers may not implement explicit disconnect cleanly; still clear local state.
601+ }
587602
588- function toOptionalPrincipalLiteral ( principalOrNull ) {
589- const text = normalizedText ( principalOrNull ) ;
590- return text ? `(some '${ text } )` : "none" ;
603+ state . connectedAddress = null ;
604+ state . lastSignature = null ;
605+ if ( previousAddress && normalizedText ( elements . forPrincipal . value ) === previousAddress ) {
606+ elements . forPrincipal . value = "" ;
607+ }
608+ if ( previousAddress && normalizedText ( elements . transferActor . value ) === previousAddress ) {
609+ elements . transferActor . value = "" ;
610+ }
611+ setWalletStatus ( "Wallet not connected." ) ;
612+ appendLog ( previousAddress ? `Wallet disconnected: ${ previousAddress } ` : "Wallet disconnected." ) ;
591613}
592614
593- async function postReadOnlyCall ( url , sender , encodedArgs ) {
594- const response = await fetch ( url , {
595- method : "POST" ,
596- headers : { "content-type" : "application/json" } ,
597- body : JSON . stringify ( {
598- sender,
599- arguments : encodedArgs ,
600- } ) ,
601- } ) ;
602- const body = await response . json ( ) . catch ( ( ) => null ) ;
603- return { response, body } ;
604- }
615+ function getReadOnlySenderCandidates ( sender , contractAddress , network = readNetwork ( ) ) {
616+ const candidates = [ ] ;
617+ const senderText = normalizedText ( sender ) ;
618+ const contractAddressText = normalizedText ( contractAddress ) ;
605619
606- function readOnlyErrorMessage ( status , body ) {
607- if ( ! body || typeof body !== "object" ) {
608- return `Read-only call failed (${ status } )` ;
620+ if ( senderText && isAddressOnNetwork ( senderText , network ) ) {
621+ candidates . push ( senderText ) ;
622+ }
623+ if (
624+ contractAddressText &&
625+ isAddressOnNetwork ( contractAddressText , network ) &&
626+ ! candidates . includes ( contractAddressText )
627+ ) {
628+ candidates . push ( contractAddressText ) ;
629+ }
630+ if ( senderText && ! candidates . includes ( senderText ) ) {
631+ candidates . push ( senderText ) ;
609632 }
610- if ( body . okay === false ) {
611- return `Read-only call returned error: ${ body . cause || "unknown" } ` ;
633+ if ( contractAddressText && ! candidates . includes ( contractAddressText ) ) {
634+ candidates . push ( contractAddressText ) ;
612635 }
613- return `Read-only call failed ( ${ status } )` ;
636+ return candidates ;
614637}
615638
616- async function fetchReadOnly ( functionName , functionArgs , sender , options = { } ) {
639+ async function fetchReadOnly ( functionName , functionArgs , sender ) {
617640 const { contractAddress, contractName } = parseContractId ( ) ;
618- const apiBase = getStacksApiBase ( ) ;
619- const url = `${ apiBase } /v2/contracts/call-read/${ contractAddress } /${ contractName } /${ functionName } ` ;
620- const hexArgs = functionArgs . map ( ( cv ) => cvHex ( cv ) ) ;
621- const firstAttempt = await postReadOnlyCall ( url , sender , hexArgs ) ;
622- if ( firstAttempt . response . ok && firstAttempt . body && firstAttempt . body . okay !== false ) {
623- return firstAttempt . body . result ;
624- }
625-
626- const clarityArgs = Array . isArray ( options . clarityArgs ) ? options . clarityArgs : null ;
627- if ( clarityArgs && clarityArgs . length === functionArgs . length ) {
628- const secondAttempt = await postReadOnlyCall ( url , sender , clarityArgs ) ;
629- if ( secondAttempt . response . ok && secondAttempt . body && secondAttempt . body . okay !== false ) {
630- appendLog ( "Read-only call retried with Clarity literal arguments." ) ;
631- return secondAttempt . body . result ;
641+ const network = createNetwork ( {
642+ network : readNetwork ( ) ,
643+ client : { baseUrl : getStacksApiBase ( ) } ,
644+ } ) ;
645+ const senders = getReadOnlySenderCandidates ( sender , contractAddress ) ;
646+
647+ let lastError = null ;
648+ for ( const senderCandidate of senders ) {
649+ try {
650+ const result = await fetchCallReadOnlyFunction ( {
651+ network,
652+ senderAddress : senderCandidate ,
653+ contractAddress,
654+ contractName,
655+ functionName,
656+ functionArgs,
657+ } ) ;
658+ if ( senderCandidate !== sender ) {
659+ appendLog ( `Read-only ${ functionName } used fallback sender=${ senderCandidate } .` ) ;
660+ }
661+ return result ;
662+ } catch ( error ) {
663+ lastError = error ;
632664 }
633- throw new Error ( readOnlyErrorMessage ( secondAttempt . response . status , secondAttempt . body ) ) ;
634665 }
635-
636- throw new Error ( readOnlyErrorMessage ( firstAttempt . response . status , firstAttempt . body ) ) ;
666+ const message =
667+ lastError instanceof Error ? lastError . message : "Read-only call failed for all sender candidates" ;
668+ throw new Error ( message ) ;
637669}
638670
639671async function handleGetPipe ( ) {
@@ -646,20 +678,15 @@ async function handleGetPipe() {
646678 "For Principal" ,
647679 elements . forPrincipal . value || state . connectedAddress ,
648680 ) ;
649- const { cv : tokenCV , tokenText } = parseOptionalTokenCV ( ) ;
681+ const { cv : tokenCV } = parseOptionalTokenCV ( ) ;
650682
651- const resultHex = await fetchReadOnly (
683+ const resultCv = await fetchReadOnly (
652684 "get-pipe" ,
653685 [ tokenCV , Cl . principal ( withPrincipal ) ] ,
654686 forPrincipal ,
655- {
656- clarityArgs : [
657- toOptionalPrincipalLiteral ( tokenText ) ,
658- toClarityPrincipalLiteral ( withPrincipal ) ,
659- ] ,
660- } ,
661687 ) ;
662- const decoded = decodeReadOnlyResult ( resultHex ) ;
688+ const resultHex = cvHex ( resultCv ) ;
689+ const decoded = unwrapClarityJson ( cvToJSON ( resultCv ) ) ;
663690 setOutput ( {
664691 call : "get-pipe" ,
665692 forPrincipal,
@@ -836,6 +863,7 @@ async function handleSignTransfer() {
836863 await ensureWallet ( { interactive : true } ) ;
837864 const context = await buildTransferContext ( ) ;
838865 const response = await request ( "stx_signStructuredMessage" , {
866+ network : readNetwork ( ) ,
839867 domain : context . domain ,
840868 message : context . message ,
841869 } ) ;
@@ -857,7 +885,7 @@ async function handleSignTransfer() {
857885 actor : context . actor ,
858886 hashedSecret : context . hashedSecret ,
859887 validAfter : context . validAfter ,
860- theirSignature : signature ,
888+ signature,
861889 } ;
862890 state . lastPayload = payload ;
863891 setOutput ( payload ) ;
@@ -884,7 +912,7 @@ async function handleBuildPayload() {
884912 actor : context . actor ,
885913 hashedSecret : context . hashedSecret ,
886914 validAfter : context . validAfter ,
887- theirSignature : state . lastSignature ,
915+ signature : state . lastSignature ,
888916 } ;
889917 state . lastPayload = payload ;
890918 setOutput ( payload ) ;
@@ -908,6 +936,7 @@ async function handleCopyOutput() {
908936
909937function wireEvents ( ) {
910938 elements . connectWallet . addEventListener ( "click" , handleConnectWallet ) ;
939+ elements . disconnectWallet . addEventListener ( "click" , handleDisconnectWallet ) ;
911940 elements . getPipe . addEventListener ( "click" , handleGetPipe ) ;
912941 elements . openPipe . addEventListener ( "click" , handleOpenPipe ) ;
913942 elements . forceCancel . addEventListener ( "click" , handleForceCancel ) ;
0 commit comments