Skip to content

Commit df1f0ce

Browse files
authored
Merge branch 'master' into s3
2 parents 366ad61 + 211c35c commit df1f0ce

File tree

25 files changed

+1168
-57
lines changed

25 files changed

+1168
-57
lines changed

apps/remix-ide/src/app/panels/file-panel.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ export default class Filepanel extends ViewPlugin {
314314
if (workspace.name !== ' - connect to localhost - ') {
315315
localStorage.setItem('currentWorkspace', workspace.name)
316316
}
317+
console.log('setting workspace', workspace)
317318
this.emit('setWorkspace', workspace)
318319
}
319320

apps/remix-ide/src/app/plugins/ai-dapp-generator.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,12 @@ export class AIDappGenerator extends Plugin {
194194

195195
if (Object.keys(pages).length === 0) {
196196
console.error('[DEBUG-AI] ❌ CRITICAL: parsePages returned empty object!');
197+
console.error('[DEBUG-AI] Raw response length:', htmlContent?.length);
198+
console.error('[DEBUG-AI] First 500 chars:', htmlContent?.substring(0, 500));
199+
console.error('[DEBUG-AI] Contains START_TITLE?', htmlContent?.includes('START_TITLE'));
200+
console.error('[DEBUG-AI] Contains <<<:', htmlContent?.includes('<<<'));
201+
const debugMatches = htmlContent?.match(/<{3,}\s*START_TITLE\s+(.*?)\s+>{3,}/g);
202+
console.error('[DEBUG-AI] Marker matches found:', debugMatches?.length || 0, debugMatches);
197203
throw new Error("AI generated empty content. Please try again.");
198204
}
199205

@@ -504,7 +510,7 @@ const ensureCompleteHtml = (html: string): string => {
504510

505511
const parsePages = (content: string) => {
506512
const pages: Record<string, string> = {}
507-
const markerRegex = /<{3,}\s*START_TITLE\s+(.*?)\s+>{3,}\s*END_TITLE/g
513+
const markerRegex = /<{3,}\s*START_TITLE\s+(.*?)\s+>{3,}(?:\s*END_TITLE)?/g
508514

509515
const parts = content.split(markerRegex)
510516

apps/remix-ide/src/app/plugins/auth-plugin.tsx

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ const profile = {
77
name: 'auth',
88
displayName: 'Authentication',
99
description: 'Handles SSO authentication and credits',
10-
methods: ['login', 'logout', 'getUser', 'getCredits', 'refreshCredits', 'linkAccount', 'getLinkedAccounts', 'unlinkAccount', 'getApiClient', 'getSSOApi', 'getCreditsApi', 'getPermissionsApi', 'getBillingApi', 'checkPermission', 'hasPermission', 'getAllPermissions', 'checkPermissions', 'getFeaturesByCategory', 'getFeatureLimit', 'getPaddleConfig', 'getToken', 'isAuthenticated', 'getInviteApi', 'validateInviteToken', 'redeemInviteToken', 'getPendingInviteToken', 'setPendingInviteToken', 'setPendingInviteValidation', 'clearPendingInviteToken', 'getPendingInviteValidation'],
11-
events: ['authStateChanged', 'creditsUpdated', 'accountLinked', 'tokenRefreshed', 'inviteTokenDetected', 'inviteTokenRedeemed']
10+
methods: ['login', 'logout', 'getUser', 'getCredits', 'refreshCredits', 'linkAccount', 'getLinkedAccounts', 'unlinkAccount', 'getApiClient', 'getSSOApi', 'getCreditsApi', 'getPermissionsApi', 'getBillingApi', 'checkPermission', 'hasPermission', 'getAllPermissions', 'checkPermissions', 'getFeaturesByCategory', 'getFeatureLimit', 'getPaddleConfig', 'fetchGitHubToken', 'disconnectGitHub', 'getInviteApi', 'validateInviteToken', 'redeemInviteToken', 'getPendingInviteToken', 'setPendingInviteToken', 'setPendingInviteValidation', 'clearPendingInviteToken', 'getPendingInviteValidation', 'isAuthenticated', 'getToken'],
11+
events: ['authStateChanged', 'creditsUpdated', 'accountLinked', 'gitHubTokenReady', 'inviteTokenDetected', 'inviteTokenRedeemed']
1212
}
1313

1414
export class AuthPlugin extends Plugin {
@@ -276,7 +276,7 @@ export class AuthPlugin extends Plugin {
276276
}
277277

278278
// Wait for message from popup
279-
const result = await new Promise<{ user: AuthUser; accessToken: string; refreshToken: string }>((resolve, reject) => {
279+
const result = await new Promise<{ user: AuthUser; accessToken: string; refreshToken: string; providerToken?: string }>((resolve, reject) => {
280280
const timeout = setTimeout(() => {
281281
cleanup()
282282
reject(new Error('Login timeout'))
@@ -291,20 +291,22 @@ export class AuthPlugin extends Plugin {
291291
}, 500) // Check every 500ms
292292

293293
const handleMessage = (event: MessageEvent) => {
294+
console.log('[AuthPlugin] Received message event:', event)
294295
// Verify origin
295296
if (event.origin !== new URL(endpointUrls.sso).origin) {
296297
return
297298
}
298299

299300
if (event.data.type === 'sso-auth-success') {
300301
console.log('[AuthPlugin] Received auth success from popup')
301-
console.log('[AuthPlugin] User data from popup:', event.data.user)
302+
console.log('[AuthPlugin] User data from popup:', event.data)
302303
console.log('[AuthPlugin] User provider field:', event.data.user?.provider)
303304
cleanup()
304305
resolve({
305306
user: event.data.user,
306307
accessToken: event.data.accessToken,
307-
refreshToken: event.data.refreshToken
308+
refreshToken: event.data.refreshToken,
309+
providerToken: event.data.providerToken
308310
})
309311
} else if (event.data.type === 'sso-auth-error') {
310312
cleanup()
@@ -325,6 +327,7 @@ export class AuthPlugin extends Plugin {
325327
})
326328

327329
// Store tokens in localStorage
330+
console.log(result)
328331
console.log('[AuthPlugin] Storing user in localStorage:', result.user)
329332
console.log('[AuthPlugin] User has provider field:', result.user.provider)
330333
localStorage.setItem('remix_access_token', result.accessToken)
@@ -342,6 +345,12 @@ export class AuthPlugin extends Plugin {
342345
token: result.accessToken
343346
})
344347

348+
// If logged in via GitHub, bridge the provider token to dgit config
349+
if (result.user.provider === 'github' && result.providerToken) {
350+
console.log('[AuthPlugin] GitHub provider detected, bridging token to dgit')
351+
await this.bridgeGitHubToken(result.providerToken)
352+
}
353+
345354
// Fetch credits after successful login
346355
this.refreshCredits().catch(console.error)
347356

@@ -409,7 +418,7 @@ export class AuthPlugin extends Plugin {
409418
}
410419

411420
// Wait for message from popup
412-
const result = await new Promise<{ user: AuthUser; accessToken: string }>((resolve, reject) => {
421+
const result = await new Promise<{ user: AuthUser; accessToken: string; providerToken?: string }>((resolve, reject) => {
413422
const timeout = setTimeout(() => {
414423
cleanup()
415424
reject(new Error('Account linking timeout'))
@@ -477,12 +486,81 @@ export class AuthPlugin extends Plugin {
477486
localStorage.setItem('remix_access_token', currentToken)
478487
localStorage.setItem('remix_user', JSON.stringify(currentUser))
479488

489+
// If linking GitHub, bridge the provider token to dgit config
490+
if (provider === 'github' && result.providerToken) {
491+
console.log('[AuthPlugin] GitHub linked, bridging token to dgit')
492+
await this.bridgeGitHubToken(result.providerToken)
493+
}
494+
480495
} catch (error: any) {
481496
console.error('[AuthPlugin] Account linking failed:', error)
482497
throw error
483498
}
484499
}
485500

501+
/**
502+
* Bridge a GitHub OAuth token to the dgit plugin config.
503+
* Saves the token and emits an event so git listeners can update state.
504+
*/
505+
private async bridgeGitHubToken(token: string): Promise<void> {
506+
try {
507+
await this.call('config' as any, 'setAppParameter', 'settings/gist-access-token', token)
508+
this.emit('gitHubTokenReady' as any, { token })
509+
console.log('[AuthPlugin] GitHub token bridged to dgit config')
510+
} catch (error) {
511+
console.error('[AuthPlugin] Failed to bridge GitHub token:', error)
512+
}
513+
}
514+
515+
/**
516+
* Fetch the stored GitHub OAuth token from the SSO backend.
517+
* Use this when a non-GitHub SSO user links GitHub later,
518+
* or to re-fetch after session restore.
519+
*/
520+
async fetchGitHubToken(): Promise<string | null> {
521+
try {
522+
const token = await this.getToken()
523+
if (!token) return null
524+
525+
const response = await fetch(`${endpointUrls.sso}/accounts/github/token`, {
526+
headers: {
527+
'Authorization': `Bearer ${token}`,
528+
'Accept': 'application/json'
529+
},
530+
credentials: 'include'
531+
})
532+
533+
if (!response.ok) {
534+
console.log('[AuthPlugin] No GitHub token available from backend:', response.status)
535+
return null
536+
}
537+
538+
const data = await response.json()
539+
if (data.access_token) {
540+
await this.bridgeGitHubToken(data.access_token)
541+
return data.access_token
542+
}
543+
return null
544+
} catch (error) {
545+
console.error('[AuthPlugin] Failed to fetch GitHub token:', error)
546+
return null
547+
}
548+
}
549+
550+
/**
551+
* Disconnect GitHub from dgit. Clears the stored GitHub token
552+
* but does NOT affect SSO login state.
553+
*/
554+
async disconnectGitHub(): Promise<void> {
555+
try {
556+
await this.call('config' as any, 'setAppParameter', 'settings/gist-access-token', '')
557+
this.emit('gitHubTokenReady' as any, { token: null })
558+
console.log('[AuthPlugin] GitHub disconnected from dgit')
559+
} catch (error) {
560+
console.error('[AuthPlugin] Failed to disconnect GitHub:', error)
561+
}
562+
}
563+
486564
async getUser(): Promise<AuthUser | null> {
487565
try {
488566
const userStr = localStorage.getItem('remix_user')

apps/remix-ide/src/app/plugins/prompt-blocks.ts

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ export const invariants = {
140140
3. **ALWAYS import React from 'react'** in any file using JSX (especially \`src/main.jsx\` and \`src/App.jsx\`).
141141
- Example: \`import React from 'react';\` must be at the top, even if you use \`createRoot\`.
142142
4. **ETHERS.JS PROVIDER RULES (CRITICAL):**
143-
- **MUST USE:** Always use \`new ethers.BrowserProvider(window.ethereum)\` for both reading and writing.
143+
- **MUST USE:** Always use \`ethers.BrowserProvider\` with a wallet provider for both reading and writing.
144+
- **PROVIDER ACQUISITION:** Use \`window.__qdapp_getProvider ? await window.__qdapp_getProvider() : window.ethereum\` to get the provider. Store this raw provider in a ref/variable for reuse (e.g. network switching).
144145
- **FORBIDDEN:** NEVER use \`new ethers.JsonRpcProvider\`, \`InfuraProvider\`, or \`AlchemyProvider\`.
145146
- **FORBIDDEN:** NEVER generate code containing placeholders like 'YOUR_INFURA_KEY' or ask for API keys.
146147
4. Use React with JSX syntax (not "text/babel" scripts).
@@ -196,7 +197,9 @@ ${functionNames}
196197
},
197198

198199
/** Wallet connection and network switching patterns */
199-
wallet: (): string => `
200+
wallet: (isLocalVM: boolean = false): string => {
201+
if (isLocalVM) {
202+
return `
200203
**WALLET CONNECTION RULES:**
201204
1. **Connect Wallet** button must be visible when disconnected.
202205
2. Check \`window.ethereum\` existence before any wallet operations.
@@ -222,15 +225,97 @@ const switchNetwork = async (targetChainHex) => {
222225
}
223226
};
224227
\`\`\`
225-
`,
228+
`
229+
}
230+
231+
// Real network: full wallet selection rules with disconnect/switch/localStorage
232+
return `
233+
**WALLET CONNECTION RULES:**
234+
1. **Connect Wallet** button must be visible in the header/navbar when disconnected.
235+
2. **Disconnect Wallet** button must be visible in the header/navbar when connected (next to the account address).
236+
3. **Switch Network** button must appear **only when** the connected wallet's chain ID differs from the DApp's target chain ID. Hide it when on the correct network.
237+
4. Use loading spinners for async actions.
238+
5. Handle "User rejected request" errors gracefully.
239+
6. Show truncated wallet address (e.g. \`0x1234...5678\`) when connected.
240+
241+
**WALLET PROVIDER ACQUISITION (CRITICAL):**
242+
The deployed DApp uses \`window.__qdapp_getProvider()\` to discover and select wallets via EIP-6963.
243+
Always get the raw provider like this:
244+
\`\`\`javascript
245+
const rawProvider = window.__qdapp_getProvider
246+
? await window.__qdapp_getProvider()
247+
: window.ethereum;
248+
if (!rawProvider) {
249+
alert('Please install a Web3 wallet (e.g. MetaMask).');
250+
return;
251+
}
252+
const provider = new ethers.BrowserProvider(rawProvider);
253+
\`\`\`
254+
**Store \`rawProvider\` in a React ref** (e.g. \`rawProviderRef.current = rawProvider\`) so you can reuse it for network switching without calling \`__qdapp_getProvider\` again.
255+
256+
**🚨 CHAIN ID COMPARISON (CRITICAL — prevents wrong-network false positive):**
257+
- ethers.js v6 returns \`network.chainId\` as a **BigInt** (e.g. \`11155111n\`).
258+
- **NEVER compare hex strings directly** (e.g. \`"0xaa36a7" !== "aa36a7"\` — prefix mismatch!).
259+
- **ALWAYS compare as decimal numbers:**
260+
\`\`\`javascript
261+
const TARGET_CHAIN_ID = 11155111; // Sepolia — use DECIMAL number
262+
// After connecting:
263+
const network = await provider.getNetwork();
264+
const currentChainId = Number(network.chainId);
265+
setChainId(currentChainId);
266+
// Wrong network check:
267+
const isWrongNetwork = account && chainId !== null && chainId !== TARGET_CHAIN_ID;
268+
\`\`\`
269+
- For \`wallet_switchEthereumChain\`, convert to hex: \`'0x' + TARGET_CHAIN_ID.toString(16)\`
270+
271+
**Disconnect Pattern (mandatory — MUST implement this):**
272+
\`\`\`javascript
273+
const disconnectWallet = () => {
274+
setAccount(null);
275+
setProvider(null);
276+
setSigner(null);
277+
rawProviderRef.current = null;
278+
// Clear saved wallet preference for wallet selection
279+
try { localStorage.removeItem('__qdapp_wallet_rdns'); } catch(e) {}
280+
};
281+
\`\`\`
282+
The Disconnect button should be placed in the navbar/header, visible when connected.
283+
When disconnected, the DApp should return to the initial "Connect Wallet" state.
284+
285+
**Network Switch Pattern (mandatory — MUST be a visible button):**
286+
Use the stored \`rawProviderRef.current\` for network operations:
287+
\`\`\`javascript
288+
const switchNetwork = async (targetChainHex) => {
289+
const rp = rawProviderRef.current;
290+
if (!rp) return;
291+
try {
292+
await rp.request({
293+
method: 'wallet_switchEthereumChain',
294+
params: [{ chainId: targetChainHex }],
295+
});
296+
} catch (switchError) {
297+
if (switchError.code === 4902) {
298+
await rp.request({ method: 'wallet_addEthereumChain', params: [...] });
299+
} else {
300+
throw switchError;
301+
}
302+
}
303+
};
304+
\`\`\`
305+
Show a **"Switch to [Network Name]"** button when the user is on the wrong chain.
306+
`
307+
},
226308

227309
/** Ethers.js v6 specific rules */
228310
ethersRules: (): string => `
229311
**ETHERS.JS v6 RULES (CRITICAL):**
230312
- **Read-Only:** For 'view'/'pure' functions, use \`new ethers.Contract(addr, abi, provider)\`.
231313
- **Write (Transaction):** For 'nonpayable'/'payable' functions, YOU MUST USE A SIGNER:
232314
\`\`\`javascript
233-
const provider = new ethers.BrowserProvider(window.ethereum);
315+
const rawProvider = window.__qdapp_getProvider
316+
? await window.__qdapp_getProvider()
317+
: window.ethereum;
318+
const provider = new ethers.BrowserProvider(rawProvider);
234319
const signer = await provider.getSigner();
235320
const contractWithSigner = new ethers.Contract(address, abi, signer);
236321
const tx = await contractWithSigner.functionName(args);
@@ -533,7 +618,7 @@ export const buildSystemPrompt = (ctx: PromptContext): string => {
533618
invariants.truncationPrevention(),
534619
// Layer 1
535620
blockchain.ethersRules(),
536-
blockchain.wallet(),
621+
blockchain.wallet(!!ctx.isLocalVM),
537622
blockchain.networkContext(ctx.contract.chainId, !!ctx.isLocalVM),
538623
// Layer 2
539624
ctx.isBaseMiniApp ? platform.baseMiniApp() : '',

0 commit comments

Comments
 (0)