@@ -21,6 +21,10 @@ export type SwapCommandOptions = {
2121 password ?: string ;
2222 walletName ?: string ;
2323
24+ // Trusted accounts (captcha bypass)
25+ trustedWalletName ?: string ;
26+ trustedPrivateKey ?: string ;
27+
2428 // Operation selection
2529 liquidity ?: boolean ;
2630 pegin ?: boolean ;
@@ -127,7 +131,7 @@ async function createBlockchainConnectionFromWallet(params: {
127131 walletsData ?: WalletData ;
128132 password ?: string ;
129133 spinner ?: SpinnerWrapper ;
130- } ) : Promise < { rskAddress : string ; connection : BlockchainConnection } > {
134+ } ) : Promise < { rskAddress : string ; connection : BlockchainConnection ; resolvedWalletName : string } > {
131135 const isTestnet = params . testnet ;
132136 const rpcUrl = getRskRpcUrl ( isTestnet ) ;
133137
@@ -160,6 +164,7 @@ async function createBlockchainConnectionFromWallet(params: {
160164 return {
161165 rskAddress : toFlyoverRskAddress ( wallet . address , isTestnet ) ,
162166 connection,
167+ resolvedWalletName : params . walletName ! ,
163168 } ;
164169 }
165170
@@ -193,6 +198,39 @@ async function createBlockchainConnectionFromWallet(params: {
193198 return {
194199 rskAddress : toFlyoverRskAddress ( wallet . address , isTestnet ) ,
195200 connection,
201+ resolvedWalletName,
202+ } ;
203+ }
204+
205+ async function createBlockchainConnectionFromPrivateKey ( params : {
206+ testnet : boolean ;
207+ privateKey : string ;
208+ } ) : Promise < { rskAddress : string ; connection : BlockchainConnection } > {
209+ const isTestnet = params . testnet ;
210+ const rpcUrl = getRskRpcUrl ( isTestnet ) ;
211+ const privateKey = params . privateKey . trim ( ) . startsWith ( "0x" )
212+ ? params . privateKey . trim ( )
213+ : `0x${ params . privateKey . trim ( ) } ` ;
214+
215+ const provider = new ethers . providers . JsonRpcProvider ( rpcUrl ) ;
216+ const walletObj = new ethers . Wallet ( privateKey , provider ) ;
217+
218+ // bridges-core-sdk expects encrypted JSON + password.
219+ // We'll generate a throwaway password since we already have the raw private key.
220+ const tempPassword = crypto . randomBytes ( 16 ) . toString ( "hex" ) ;
221+ const encryptedJson = await walletObj . encrypt ( tempPassword ) ;
222+ const encryptedJsonObj =
223+ typeof encryptedJson === "string" ? JSON . parse ( encryptedJson ) : encryptedJson ;
224+
225+ const connection = await BlockchainConnection . createUsingEncryptedJson (
226+ encryptedJsonObj ,
227+ tempPassword ,
228+ rpcUrl
229+ ) ;
230+
231+ return {
232+ rskAddress : toFlyoverRskAddress ( walletObj . address , isTestnet ) ,
233+ connection,
196234 } ;
197235}
198236
@@ -206,6 +244,22 @@ function toFlyoverRskAddress(address: string, isTestnet: boolean): string {
206244 return FlyoverUtils . rskChecksum ( address , chainId ) ;
207245}
208246
247+ function normalizeWalletLabel ( name ?: string ) : string {
248+ return String ( name || "" ) . trim ( ) . toLowerCase ( ) ;
249+ }
250+
251+ function rethrowIfTrustedAccountRejected ( error : unknown ) : never {
252+ const msg = error instanceof Error ? error . message : String ( error ) ;
253+ if ( / t r u s t e d a c c o u n t / i. test ( msg ) || / f e t c h i n g t r u s t e d / i. test ( msg ) ) {
254+ throw new Error (
255+ "The liquidity provider rejected authenticated (trusted) quote acceptance. " +
256+ "Your Rootstock address must be allowlisted on that LP, or use a valid captcha token (set FLYOVER_CAPTCHA_TOKEN) and run without --trusted-wallet. " +
257+ "Ask the LP operator to add your address to trusted accounts on their LPS."
258+ ) ;
259+ }
260+ throw error instanceof Error ? error : new Error ( msg ) ;
261+ }
262+
209263async function resolveCaptchaToken ( params : {
210264 isExternal : boolean ;
211265 providerName ?: string ;
@@ -319,6 +373,65 @@ function selectProvider(params: {
319373 return providers . find ( ( p ) => ! p ?. siteKey ) ?? providers [ 0 ] ;
320374}
321375
376+ async function createTrustedFlyoverSigner ( params : {
377+ isTestnet : boolean ;
378+ isExternal : boolean ;
379+ trustedWalletName ?: string ;
380+ trustedPrivateKey ?: string ;
381+ walletsData ?: WalletData ;
382+ password ?: string ;
383+ spinner ?: SpinnerWrapper ;
384+ /** When the trusted wallet is the same as the signing wallet, reuse this connection (one password prompt). */
385+ reuseSigner ?: { rskAddress : string ; connection : BlockchainConnection } ;
386+ } ) : Promise < { trustedAddress : string ; trustedFlyover : Flyover } | null > {
387+ const trustedWalletName = ( params . trustedWalletName || "" ) . trim ( ) ;
388+ const trustedPrivateKey = ( params . trustedPrivateKey || "" ) . trim ( ) ;
389+
390+ if ( ! trustedWalletName && ! trustedPrivateKey ) return null ;
391+
392+ if ( trustedWalletName && trustedPrivateKey ) {
393+ throw new Error ( "Please provide only one of trustedWalletName or trustedPrivateKey." ) ;
394+ }
395+
396+ let rskAddress : string ;
397+ let connection : BlockchainConnection ;
398+
399+ if ( trustedPrivateKey ) {
400+ const created = await createBlockchainConnectionFromPrivateKey ( {
401+ testnet : params . isTestnet ,
402+ privateKey : trustedPrivateKey ,
403+ } ) ;
404+ rskAddress = created . rskAddress ;
405+ connection = created . connection ;
406+ } else if ( params . reuseSigner && trustedWalletName ) {
407+ rskAddress = params . reuseSigner . rskAddress ;
408+ connection = params . reuseSigner . connection ;
409+ } else if ( trustedWalletName ) {
410+ const created = await createBlockchainConnectionFromWallet ( {
411+ testnet : params . isTestnet ,
412+ isExternal : params . isExternal ,
413+ walletName : trustedWalletName ,
414+ walletsData : params . walletsData ,
415+ password : params . password ,
416+ spinner : params . spinner ,
417+ } ) ;
418+ rskAddress = created . rskAddress ;
419+ connection = created . connection ;
420+ } else {
421+ return null ;
422+ }
423+
424+ const trustedFlyover = new Flyover ( {
425+ network : getFlyoverNetwork ( params . isTestnet ) ,
426+ rskConnection : connection ,
427+ // Not used in authenticated acceptance flow, but required by constructor.
428+ captchaTokenResolver : async ( ) => "" ,
429+ allowInsecureConnections : false ,
430+ } ) ;
431+
432+ return { trustedAddress : rskAddress , trustedFlyover } ;
433+ }
434+
322435async function handlePegIn ( _params : {
323436 isTestnet : boolean ;
324437 amount : number ;
@@ -328,6 +441,8 @@ async function handlePegIn(_params: {
328441 password ?: string ;
329442 spinner ?: SpinnerWrapper ;
330443 provider ?: string ;
444+ trustedWalletName ?: string ;
445+ trustedPrivateKey ?: string ;
331446} ) : Promise < Pick <
332447 NonNullable < SwapResult [ "data" ] > ,
333448 "txHash" | "totalFeeEstimate" | "quoteExpiresAt" | "depositAddress"
@@ -339,7 +454,7 @@ async function handlePegIn(_params: {
339454 _params ;
340455 const amountWei = parseUnits ( amount . toString ( ) , 18 ) ;
341456
342- const { rskAddress, connection } = await createBlockchainConnectionFromWallet ( {
457+ const { rskAddress, connection, resolvedWalletName } = await createBlockchainConnectionFromWallet ( {
343458 testnet : isTestnet ,
344459 isExternal,
345460 walletName,
@@ -348,6 +463,11 @@ async function handlePegIn(_params: {
348463 spinner,
349464 } ) ;
350465
466+ const reuseTrustedConnection =
467+ ! _params . trustedPrivateKey &&
468+ ! ! _params . trustedWalletName ?. trim ( ) &&
469+ normalizeWalletLabel ( _params . trustedWalletName ) === normalizeWalletLabel ( resolvedWalletName ) ;
470+
351471 let selectedProvider : any ;
352472 const captchaTokenResolver = async ( ) =>
353473 resolveCaptchaToken ( {
@@ -416,7 +536,30 @@ async function handlePegIn(_params: {
416536 logInfo ( isExternal , `⛽ Required confirmations: ${ quote . quote . confirmations } ` ) ;
417537 logInfo ( isExternal , `⏳ Quote expires at: ${ quoteExpiresAt } ` ) ;
418538
419- const accepted = await flyover . acceptQuote ( quote ) ;
539+ const trusted = await createTrustedFlyoverSigner ( {
540+ isTestnet,
541+ isExternal,
542+ trustedWalletName : _params . trustedWalletName ,
543+ trustedPrivateKey : _params . trustedPrivateKey ,
544+ walletsData,
545+ password,
546+ spinner,
547+ reuseSigner : reuseTrustedConnection ? { rskAddress, connection } : undefined ,
548+ } ) ;
549+
550+ let accepted : any ;
551+ if ( trusted ) {
552+ trusted . trustedFlyover . useLiquidityProvider ( selectedProvider ) ;
553+ const signature = await ( trusted . trustedFlyover as any ) . signQuote ( quote ) ;
554+ logInfo ( isExternal , `🔐 Using trusted account to accept quote: ${ trusted . trustedAddress } ` ) ;
555+ try {
556+ accepted = await ( flyover as any ) . acceptAuthenticatedQuote ( quote , signature ) ;
557+ } catch ( e ) {
558+ rethrowIfTrustedAccountRejected ( e ) ;
559+ }
560+ } else {
561+ accepted = await flyover . acceptQuote ( quote ) ;
562+ }
420563 void accepted ; // signature is not used directly by this CLI for peg-in display
421564
422565 const status = await flyover . getPeginStatus ( quote . quoteHash ) ;
@@ -455,6 +598,8 @@ async function handlePegOut(_params: {
455598 password ?: string ;
456599 spinner ?: SpinnerWrapper ;
457600 provider ?: string ;
601+ trustedWalletName ?: string ;
602+ trustedPrivateKey ?: string ;
458603} ) : Promise < Pick <
459604 NonNullable < SwapResult [ "data" ] > ,
460605 "txHash" | "totalFeeEstimate" | "quoteExpiresAt"
@@ -487,7 +632,7 @@ async function handlePegOut(_params: {
487632 throw new Error ( `Invalid BTC address for ${ isTestnet ? "testnet" : "mainnet" } ` ) ;
488633 }
489634
490- const { rskAddress, connection } = await createBlockchainConnectionFromWallet ( {
635+ const { rskAddress, connection, resolvedWalletName } = await createBlockchainConnectionFromWallet ( {
491636 testnet : isTestnet ,
492637 isExternal,
493638 walletName,
@@ -496,6 +641,11 @@ async function handlePegOut(_params: {
496641 spinner,
497642 } ) ;
498643
644+ const reuseTrustedConnection =
645+ ! _params . trustedPrivateKey &&
646+ ! ! _params . trustedWalletName ?. trim ( ) &&
647+ normalizeWalletLabel ( _params . trustedWalletName ) === normalizeWalletLabel ( resolvedWalletName ) ;
648+
499649 let selectedProvider : any ;
500650 const captchaTokenResolver = async ( ) =>
501651 resolveCaptchaToken ( {
@@ -551,7 +701,30 @@ async function handlePegOut(_params: {
551701 ) ;
552702 logInfo ( isExternal , `⏳ Quote expires at: ${ quoteExpiresAt } ` ) ;
553703
554- const acceptedQuote = await flyover . acceptPegoutQuote ( quote ) ;
704+ const trusted = await createTrustedFlyoverSigner ( {
705+ isTestnet,
706+ isExternal,
707+ trustedWalletName : _params . trustedWalletName ,
708+ trustedPrivateKey : _params . trustedPrivateKey ,
709+ walletsData,
710+ password,
711+ spinner,
712+ reuseSigner : reuseTrustedConnection ? { rskAddress, connection } : undefined ,
713+ } ) ;
714+
715+ let acceptedQuote : any ;
716+ if ( trusted ) {
717+ trusted . trustedFlyover . useLiquidityProvider ( selectedProvider ) ;
718+ const signature = await ( trusted . trustedFlyover as any ) . signQuote ( quote ) ;
719+ logInfo ( isExternal , `🔐 Using trusted account to accept quote: ${ trusted . trustedAddress } ` ) ;
720+ try {
721+ acceptedQuote = await ( flyover as any ) . acceptAuthenticatedPegoutQuote ( quote , signature ) ;
722+ } catch ( e ) {
723+ rethrowIfTrustedAccountRejected ( e ) ;
724+ }
725+ } else {
726+ acceptedQuote = await flyover . acceptPegoutQuote ( quote ) ;
727+ }
555728 const txHash = await flyover . depositPegout ( quote , acceptedQuote . signature , totalFeeEstimateWei ) ;
556729
557730 logSuccess ( isExternal , `✅ Peg-out transaction executed. TxHash: ${ txHash } ` ) ;
@@ -714,6 +887,8 @@ export async function swapCommand(
714887 password : params . password ,
715888 spinner,
716889 provider : params . provider ,
890+ trustedWalletName : params . trustedWalletName ,
891+ trustedPrivateKey : params . trustedPrivateKey ,
717892 } ) ;
718893
719894 spinner . start ( "⏳ Finalizing peg-in..." ) ;
@@ -759,6 +934,8 @@ export async function swapCommand(
759934 password : params . password ,
760935 spinner,
761936 provider : params . provider ,
937+ trustedWalletName : params . trustedWalletName ,
938+ trustedPrivateKey : params . trustedPrivateKey ,
762939 } ) ;
763940
764941 spinner . start ( "⏳ Finalizing peg-out..." ) ;
@@ -775,7 +952,10 @@ export async function swapCommand(
775952 const errorMessage =
776953 error instanceof Error ? error . message : "Error executing swap (unknown error)." ;
777954 spinner . fail ( chalk . red ( `❌ ${ errorMessage } ` ) ) ;
778- logError ( params . isExternal || false , errorMessage ) ;
955+ // Avoid duplicating the same line on the console for non-external (MCP) runs.
956+ if ( params . isExternal ) {
957+ logError ( true , errorMessage ) ;
958+ }
779959 return { success : false , error : errorMessage } ;
780960 } finally {
781961 if ( ! params . isExternal ) {
0 commit comments