@@ -21,11 +21,23 @@ const DEFAULT_API_BY_NETWORK = {
2121} ;
2222
2323const STACKFLOW_MESSAGE_VERSION = "0.6.0" ;
24+ const BNSV2_API_BASE = "https://api.bnsv2.com" ;
25+ const CONTRACT_PRESETS = {
26+ "stx-mainnet" : {
27+ contractId : "SP126XFZQ3ZHYM6Q6KAQZMMJSDY91A8BTT6AD08RV.stackflow-0-6-0" ,
28+ tokenContract : "" ,
29+ } ,
30+ "sbtc-mainnet" : {
31+ contractId : "SP126XFZQ3ZHYM6Q6KAQZMMJSDY91A8BTT6AD08RV.stackflow-sbtc-0-6-0" ,
32+ tokenContract : "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token" ,
33+ } ,
34+ } ;
2435
2536const elements = {
2637 network : document . getElementById ( "network" ) ,
2738 stacksApiUrl : document . getElementById ( "stacks-api-url" ) ,
2839 contractId : document . getElementById ( "contract-id" ) ,
40+ contractPreset : document . getElementById ( "contract-preset" ) ,
2941 counterparty : document . getElementById ( "counterparty" ) ,
3042 tokenContract : document . getElementById ( "token-contract" ) ,
3143 forPrincipal : document . getElementById ( "for-principal" ) ,
@@ -54,6 +66,7 @@ const state = {
5466 connectedAddress : null ,
5567 lastSignature : null ,
5668 lastPayload : null ,
69+ nameCache : new Map ( ) ,
5770} ;
5871
5972function normalizedText ( value ) {
@@ -64,6 +77,32 @@ function isStacksAddress(value) {
6477 return / ^ S [ P M T ] [ A - Z 0 - 9 ] { 38 , 42 } $ / i. test ( normalizedText ( value ) ) ;
6578}
6679
80+ function isPrincipalText ( value ) {
81+ const text = normalizedText ( value ) ;
82+ if ( ! text || ! / ^ S / i. test ( text ) ) {
83+ return false ;
84+ }
85+ try {
86+ principalCV ( text ) ;
87+ return true ;
88+ } catch {
89+ return false ;
90+ }
91+ }
92+
93+ function getStacksApiBase ( ) {
94+ const apiBase = normalizedText ( elements . stacksApiUrl . value ) . replace ( / \/ + $ / , "" ) ;
95+ if ( ! apiBase ) {
96+ throw new Error ( "Stacks API URL is required" ) ;
97+ }
98+ return apiBase ;
99+ }
100+
101+ function looksLikeBtcName ( value ) {
102+ const text = normalizedText ( value ) . toLowerCase ( ) ;
103+ return / ^ [ a - z 0 - 9 ] [ a - z 0 - 9 - ] { 0 , 36 } \. b t c $ / . test ( text ) ;
104+ }
105+
67106function nowStamp ( ) {
68107 return new Date ( ) . toISOString ( ) . slice ( 11 , 19 ) ;
69108}
@@ -176,12 +215,131 @@ function readNetwork() {
176215 return network ;
177216}
178217
179- function parseRequiredPrincipal ( fieldName , value ) {
180- const principal = normalizedText ( value ) ;
181- if ( ! isStacksAddress ( principal ) ) {
182- throw new Error ( `${ fieldName } must be a valid Stacks principal` ) ;
218+ function extractPrincipalFromNamePayload ( payload ) {
219+ const visited = new Set ( ) ;
220+ const crawl = ( value ) => {
221+ if ( value == null ) {
222+ return null ;
223+ }
224+ if ( typeof value === "string" ) {
225+ return isPrincipalText ( value ) ? value : null ;
226+ }
227+ if ( typeof value !== "object" ) {
228+ return null ;
229+ }
230+ if ( visited . has ( value ) ) {
231+ return null ;
232+ }
233+ visited . add ( value ) ;
234+
235+ if ( Array . isArray ( value ) ) {
236+ for ( const entry of value ) {
237+ const found = crawl ( entry ) ;
238+ if ( found ) {
239+ return found ;
240+ }
241+ }
242+ return null ;
243+ }
244+
245+ const priorityKeys = [
246+ "address" ,
247+ "owner" ,
248+ "owner_address" ,
249+ "ownerAddress" ,
250+ "current_owner" ,
251+ "principal" ,
252+ ] ;
253+ for ( const key of priorityKeys ) {
254+ if ( key in value ) {
255+ const found = crawl ( value [ key ] ) ;
256+ if ( found ) {
257+ return found ;
258+ }
259+ }
260+ }
261+ for ( const nested of Object . values ( value ) ) {
262+ const found = crawl ( nested ) ;
263+ if ( found ) {
264+ return found ;
265+ }
266+ }
267+ return null ;
268+ } ;
269+
270+ return crawl ( payload ) ;
271+ }
272+
273+ async function resolveBtcNameToPrincipal ( name ) {
274+ const normalizedName = normalizedText ( name ) . toLowerCase ( ) ;
275+ const network = readNetwork ( ) ;
276+ const cacheKey = `${ network } :${ normalizedName } ` ;
277+ const cached = state . nameCache . get ( cacheKey ) ;
278+ if ( cached ) {
279+ return cached ;
280+ }
281+
282+ const encoded = encodeURIComponent ( normalizedName ) ;
283+ const endpoints =
284+ network === "mainnet"
285+ ? [ `${ BNSV2_API_BASE } /names/${ encoded } ` ]
286+ : network === "testnet"
287+ ? [ `${ BNSV2_API_BASE } /testnet/names/${ encoded } ` ]
288+ : [ `${ BNSV2_API_BASE } /testnet/names/${ encoded } ` , `${ BNSV2_API_BASE } /names/${ encoded } ` ] ;
289+
290+ const failures = [ ] ;
291+ for ( const endpoint of endpoints ) {
292+ try {
293+ const response = await fetch ( endpoint , {
294+ headers : { accept : "application/json" } ,
295+ } ) ;
296+ if ( response . status === 404 ) {
297+ failures . push ( `${ response . status } ${ endpoint } ` ) ;
298+ continue ;
299+ }
300+ if ( ! response . ok ) {
301+ failures . push ( `${ response . status } ${ endpoint } ` ) ;
302+ continue ;
303+ }
304+ const body = await response . json ( ) . catch ( ( ) => null ) ;
305+ const principal = extractPrincipalFromNamePayload ( body ) ;
306+ if ( principal ) {
307+ state . nameCache . set ( cacheKey , principal ) ;
308+ return principal ;
309+ }
310+ failures . push ( `no-principal ${ endpoint } ` ) ;
311+ } catch ( error ) {
312+ failures . push (
313+ `error ${ endpoint } : ${ error instanceof Error ? error . message : String ( error ) } ` ,
314+ ) ;
315+ }
316+ }
317+
318+ throw new Error (
319+ `Could not resolve ${ normalizedName } . Tried: ${ failures . slice ( 0 , 3 ) . join ( " | " ) } ` ,
320+ ) ;
321+ }
322+
323+ async function resolvePrincipalInput ( fieldName , value , { required = true } = { } ) {
324+ const input = normalizedText ( value ) ;
325+ if ( ! input ) {
326+ if ( required ) {
327+ throw new Error ( `${ fieldName } is required` ) ;
328+ }
329+ return null ;
330+ }
331+
332+ if ( isPrincipalText ( input ) ) {
333+ return input ;
183334 }
184- return principal ;
335+
336+ if ( looksLikeBtcName ( input ) ) {
337+ const principal = await resolveBtcNameToPrincipal ( input ) ;
338+ appendLog ( `${ fieldName } : resolved ${ input } -> ${ principal } ` ) ;
339+ return principal ;
340+ }
341+
342+ throw new Error ( `${ fieldName } must be a Stacks principal or .btc name` ) ;
185343}
186344
187345function parseOptionalTokenCV ( ) {
@@ -324,6 +482,36 @@ function updateNetworkDefaults() {
324482 }
325483}
326484
485+ function getPresetKeyByValues ( contractId , tokenContract ) {
486+ const contractText = normalizedText ( contractId ) ;
487+ const tokenText = normalizedText ( tokenContract ) ;
488+ for ( const [ presetKey , preset ] of Object . entries ( CONTRACT_PRESETS ) ) {
489+ if (
490+ normalizedText ( preset . contractId ) === contractText &&
491+ normalizedText ( preset . tokenContract ) === tokenText
492+ ) {
493+ return presetKey ;
494+ }
495+ }
496+ return "custom" ;
497+ }
498+
499+ function applyContractPreset ( presetKey , { log = true } = { } ) {
500+ const preset = CONTRACT_PRESETS [ presetKey ] ;
501+ if ( ! preset ) {
502+ return ;
503+ }
504+ elements . contractId . value = preset . contractId ;
505+ elements . tokenContract . value = preset . tokenContract ;
506+ if ( log ) {
507+ appendLog (
508+ `Applied preset ${ presetKey } : contract=${ preset . contractId } , token=${
509+ preset . tokenContract || "(none)"
510+ } `,
511+ ) ;
512+ }
513+ }
514+
327515async function handleConnectWallet ( ) {
328516 try {
329517 const address = await ensureWallet ( { interactive : true } ) ;
@@ -337,10 +525,7 @@ async function handleConnectWallet() {
337525
338526async function fetchReadOnly ( functionName , functionArgs , sender ) {
339527 const { contractAddress, contractName } = parseContractId ( ) ;
340- const apiBase = normalizedText ( elements . stacksApiUrl . value ) . replace ( / \/ + $ / , "" ) ;
341- if ( ! apiBase ) {
342- throw new Error ( "Stacks API URL is required" ) ;
343- }
528+ const apiBase = getStacksApiBase ( ) ;
344529 const url = `${ apiBase } /v2/contracts/call-read/${ contractAddress } /${ contractName } /${ functionName } ` ;
345530 const response = await fetch ( url , {
346531 method : "POST" ,
@@ -365,8 +550,11 @@ async function fetchReadOnly(functionName, functionArgs, sender) {
365550
366551async function handleGetPipe ( ) {
367552 try {
368- const withPrincipal = parseRequiredPrincipal ( "Counterparty" , elements . counterparty . value ) ;
369- const forPrincipal = parseRequiredPrincipal (
553+ const withPrincipal = await resolvePrincipalInput (
554+ "Counterparty" ,
555+ elements . counterparty . value ,
556+ ) ;
557+ const forPrincipal = await resolvePrincipalInput (
370558 "For Principal" ,
371559 elements . forPrincipal . value || state . connectedAddress ,
372560 ) ;
@@ -416,7 +604,10 @@ async function handleOpenPipe() {
416604 if ( ! sender ) {
417605 throw new Error ( "Connect wallet first" ) ;
418606 }
419- const withPrincipal = parseRequiredPrincipal ( "Counterparty" , elements . counterparty . value ) ;
607+ const withPrincipal = await resolvePrincipalInput (
608+ "Counterparty" ,
609+ elements . counterparty . value ,
610+ ) ;
420611 const amount = parseUintInput ( "Amount" , elements . openAmount . value , { min : 1n } ) ;
421612 const nonceText = normalizedText ( elements . openNonce . value ) ;
422613 const nonce = nonceText ? parseUintInput ( "Nonce" , nonceText ) : 0n ;
@@ -458,7 +649,10 @@ async function handleOpenPipe() {
458649async function handleForceCancel ( ) {
459650 try {
460651 await ensureWallet ( { interactive : true } ) ;
461- const withPrincipal = parseRequiredPrincipal ( "Counterparty" , elements . counterparty . value ) ;
652+ const withPrincipal = await resolvePrincipalInput (
653+ "Counterparty" ,
654+ elements . counterparty . value ,
655+ ) ;
462656 const { cv : tokenCV } = parseOptionalTokenCV ( ) ;
463657
464658 const response = await callContract ( "force-cancel" , [
@@ -479,15 +673,18 @@ async function handleForceCancel() {
479673 }
480674}
481675
482- function buildTransferContext ( ) {
676+ async function buildTransferContext ( ) {
483677 const network = readNetwork ( ) ;
484678 const { contractId } = parseContractId ( ) ;
485- const forPrincipal = parseRequiredPrincipal (
679+ const forPrincipal = await resolvePrincipalInput (
486680 "For Principal" ,
487681 elements . forPrincipal . value || state . connectedAddress ,
488682 ) ;
489- const withPrincipal = parseRequiredPrincipal ( "Counterparty" , elements . counterparty . value ) ;
490- const actor = parseRequiredPrincipal (
683+ const withPrincipal = await resolvePrincipalInput (
684+ "Counterparty" ,
685+ elements . counterparty . value ,
686+ ) ;
687+ const actor = await resolvePrincipalInput (
491688 "Actor Principal" ,
492689 elements . transferActor . value || forPrincipal ,
493690 ) ;
@@ -543,7 +740,7 @@ function buildTransferContext() {
543740async function handleSignTransfer ( ) {
544741 try {
545742 await ensureWallet ( { interactive : true } ) ;
546- const context = buildTransferContext ( ) ;
743+ const context = await buildTransferContext ( ) ;
547744 const response = await request ( "stx_signStructuredMessage" , {
548745 domain : context . domain ,
549746 message : context . message ,
@@ -578,9 +775,9 @@ async function handleSignTransfer() {
578775 }
579776}
580777
581- function handleBuildPayload ( ) {
778+ async function handleBuildPayload ( ) {
582779 try {
583- const context = buildTransferContext ( ) ;
780+ const context = await buildTransferContext ( ) ;
584781 const payload = {
585782 contractId : context . contractId ,
586783 forPrincipal : context . forPrincipal ,
@@ -626,11 +823,35 @@ function wireEvents() {
626823 elements . network . addEventListener ( "change" , ( ) => {
627824 elements . stacksApiUrl . value = DEFAULT_API_BY_NETWORK [ readNetwork ( ) ] ;
628825 } ) ;
826+ elements . contractPreset . addEventListener ( "change" , ( ) => {
827+ applyContractPreset ( elements . contractPreset . value ) ;
828+ } ) ;
829+ elements . contractId . addEventListener ( "input" , ( ) => {
830+ elements . contractPreset . value = getPresetKeyByValues (
831+ elements . contractId . value ,
832+ elements . tokenContract . value ,
833+ ) ;
834+ } ) ;
835+ elements . tokenContract . addEventListener ( "input" , ( ) => {
836+ elements . contractPreset . value = getPresetKeyByValues (
837+ elements . contractId . value ,
838+ elements . tokenContract . value ,
839+ ) ;
840+ } ) ;
629841}
630842
631843async function bootstrap ( ) {
632844 wireEvents ( ) ;
633845 updateNetworkDefaults ( ) ;
846+ if ( ! normalizedText ( elements . contractId . value ) && ! normalizedText ( elements . tokenContract . value ) ) {
847+ elements . contractPreset . value = "stx-mainnet" ;
848+ applyContractPreset ( "stx-mainnet" , { log : false } ) ;
849+ } else {
850+ elements . contractPreset . value = getPresetKeyByValues (
851+ elements . contractId . value ,
852+ elements . tokenContract . value ,
853+ ) ;
854+ }
634855 try {
635856 const address = await ensureWallet ( { interactive : false } ) ;
636857 if ( address ) {
0 commit comments