Skip to content

Commit 6b56a82

Browse files
committed
upload dapp to ipfs
1 parent 1eff1e4 commit 6b56a82

File tree

5 files changed

+1317
-37
lines changed

5 files changed

+1317
-37
lines changed

apps/quick-dapp/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@dnd-kit/core": "^6.1.0",
88
"@dnd-kit/sortable": "^8.0.0",
99
"@drafish/surge-client": "^1.1.5",
10-
"esbuild-wasm": "^0.25.12"
10+
"esbuild-wasm": "^0.25.12",
11+
"ipfs-http-client": "^47.0.1"
1112
}
1213
}

apps/quick-dapp/src/components/DeployPanel/index.tsx

Lines changed: 248 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import {
99
import { ThemeUI } from './theme';
1010
import { CustomTooltip } from '@remix-ui/helper';
1111
import { AppContext } from '../../contexts';
12+
import IpfsHttpClient from 'ipfs-http-client';
13+
import { readDappFiles } from '../EditHtmlTemplate';
14+
import { InBrowserVite } from '../../InBrowserVite';
1215

1316
function DeployPanel(): JSX.Element {
1417
const intl = useIntl()
@@ -18,6 +21,73 @@ function DeployPanel(): JSX.Element {
1821
shortname: localStorage.getItem('__DISQUS_SHORTNAME') || '',
1922
shareTo: [],
2023
});
24+
25+
const [showIpfsSettings, setShowIpfsSettings] = useState(false);
26+
const [ipfsHost, setIpfsHost] = useState('');
27+
const [ipfsPort, setIpfsPort] = useState('');
28+
const [ipfsProtocol, setIpfsProtocol] = useState('');
29+
const [ipfsProjectId, setIpfsProjectId] = useState('');
30+
const [ipfsProjectSecret, setIpfsProjectSecret] = useState('');
31+
const [isDeploying, setIsDeploying] = useState(false);
32+
const [deployResult, setDeployResult] = useState({ cid: '', error: '' });
33+
34+
const getRemixIpfsSettings = () => {
35+
let result = null;
36+
37+
for (let i = 0; i < localStorage.length; i++) {
38+
const key = localStorage.key(i);
39+
40+
if (key && key.includes('remix.config')) {
41+
try {
42+
const data = JSON.parse(localStorage.getItem(key));
43+
44+
if (data && data["settings/ipfs-url"]) {
45+
result = {
46+
url: data["settings/ipfs-url"] || null,
47+
port: data["settings/ipfs-port"] || null,
48+
protocol: data["settings/ipfs-protocol"] || null,
49+
projectId: data["settings/ipfs-project-id"] || null,
50+
projectSecret: data["settings/ipfs-project-secret"] || null,
51+
};
52+
break;
53+
}
54+
} catch (err) {
55+
console.warn(`⚠️ ${key} JSON parse error:`, err);
56+
}
57+
}
58+
}
59+
60+
return result;
61+
}
62+
63+
useEffect(() => {
64+
const loadGlobalIpfsSettings = () => {
65+
try {
66+
const ipfsSettings = getRemixIpfsSettings();
67+
68+
if (ipfsSettings && ipfsSettings.url) {
69+
const {
70+
url: host,
71+
port,
72+
protocol,
73+
projectId: id,
74+
projectSecret: secret
75+
} = ipfsSettings;
76+
77+
setIpfsHost(host);
78+
setIpfsPort(port || '5001');
79+
setIpfsProtocol(protocol || 'https');
80+
setIpfsProjectId(id || '');
81+
setIpfsProjectSecret(secret || '');
82+
}
83+
} catch (e) {
84+
console.error(e);
85+
}
86+
setShowIpfsSettings(true);
87+
};
88+
loadGlobalIpfsSettings();
89+
}, []);
90+
2191
const setShareTo = (type: string) => {
2292
let shareTo = formVal.shareTo;
2393
if (formVal.shareTo.includes(type)) {
@@ -27,6 +97,120 @@ function DeployPanel(): JSX.Element {
2797
}
2898
setFormVal({ ...formVal, shareTo });
2999
};
100+
101+
const handleIpfsDeploy = async () => {
102+
setIsDeploying(true);
103+
setDeployResult({ cid: '', error: '' });
104+
105+
let builder: InBrowserVite;
106+
let jsResult: { js: string; success: boolean; error?: string };
107+
let filesMap: Map<string, string>;
108+
109+
try {
110+
builder = new InBrowserVite();
111+
await builder.initialize();
112+
113+
filesMap = new Map<string, string>();
114+
await readDappFiles('dapp', filesMap);
115+
116+
if (filesMap.size === 0) {
117+
throw new Error("No DApp files");
118+
}
119+
120+
jsResult = await builder.build(filesMap, '/src/main.jsx');
121+
if (!jsResult.success) {
122+
throw new Error(`DApp build failed: ${jsResult.error}`);
123+
}
124+
125+
} catch (e) {
126+
console.error(e);
127+
setDeployResult({ cid: '', error: `DApp build failed: ${e.message}` });
128+
setIsDeploying(false);
129+
return;
130+
}
131+
132+
133+
let ipfsClient;
134+
try {
135+
const auth = (ipfsProjectId && ipfsProjectSecret)
136+
? 'Basic ' + Buffer.from(ipfsProjectId + ':' + ipfsProjectSecret).toString('base64')
137+
: null;
138+
139+
const headers = auth ? { Authorization: auth } : {};
140+
141+
let clientOptions;
142+
if (ipfsHost) {
143+
clientOptions = {
144+
host: ipfsHost.replace(/^(https|http):\/\//, ''),
145+
port: parseInt(ipfsPort) || 5001,
146+
protocol: ipfsProtocol || 'https',
147+
headers
148+
};
149+
} else {
150+
clientOptions = { host: 'ipfs.infura.io', port: 5001, protocol: 'https', headers };
151+
}
152+
153+
ipfsClient = IpfsHttpClient(clientOptions);
154+
155+
} catch (e) {
156+
console.error(e);
157+
setDeployResult({ cid: '', error: `IPFS: ${e.message}` });
158+
setIsDeploying(false);
159+
return;
160+
}
161+
162+
try {
163+
const indexHtmlContent = filesMap.get('/index.html');
164+
if (!indexHtmlContent) {
165+
throw new Error("Cannot find index.html");
166+
}
167+
168+
let modifiedHtml = indexHtmlContent;
169+
170+
modifiedHtml = modifiedHtml.replace(
171+
/<script type="module"[^>]*src="(?:\/|\.\/)?src\/main\.jsx"[^>]*><\/script>/,
172+
'<script type="module" src="./app.js"></script>'
173+
);
174+
175+
modifiedHtml = modifiedHtml.replace(
176+
/<link rel="stylesheet"[^>]*href="(?:\/|\.\/)?src\/index\.css"[^>]*>/,
177+
''
178+
);
179+
180+
const filesToUpload = [
181+
{
182+
path: 'index.html',
183+
content: modifiedHtml
184+
},
185+
{
186+
path: 'app.js',
187+
content: jsResult.js
188+
}
189+
];
190+
191+
const addOptions = {
192+
wrapWithDirectory: true,
193+
cidVersion: 1,
194+
};
195+
196+
let rootCid = '';
197+
for await (const result of ipfsClient.addAll(filesToUpload, addOptions)) {
198+
if (result.path === '') {
199+
rootCid = result.cid.toString();
200+
}
201+
}
202+
203+
setDeployResult({ cid: rootCid, error: '' });
204+
setIsDeploying(false);
205+
206+
if (showIpfsSettings && ipfsHost) {
207+
}
208+
209+
} catch (e) {
210+
setDeployResult({ cid: '', error: `IPFS: ${e.message}` });
211+
setIsDeploying(false);
212+
}
213+
};
30214

31215
return (
32216
<div className="d-inline-block">
@@ -182,20 +366,72 @@ function DeployPanel(): JSX.Element {
182366
</label>
183367
</div>
184368
</Form.Group>
369+
185370
<ThemeUI />
186371

187-
<Button
188-
data-id="deployDapp-IPFS"
189-
variant="primary"
190-
type="button" // type="submit"에서 변경
191-
className="mt-3"
192-
onClick={() => {
193-
console.log("Deploying to IPFS/ENS... (TODO)");
194-
}}
195-
>
196-
<FormattedMessage id="quickDapp.deployToIPFS" defaultMessage="Deploy to IPFS & ENS" />
197-
</Button>
198-
</Form>
372+
{showIpfsSettings && (
373+
<>
374+
<hr />
375+
<h5 className="mb-2"><FormattedMessage id="quickDapp.ipfsSettings" defaultMessage="IPFS Settings" /></h5>
376+
<Alert variant="info" className="mb-2 small">
377+
<FormattedMessage id="quickDapp.ipfsSettings.info" defaultMessage="No global IPFS settings found. Please provide credentials below, or configure them in the 'Settings' plugin." />
378+
</Alert>
379+
<Form.Group className="mb-2" controlId="formIpfsHost">
380+
<Form.Label className="text-uppercase mb-0">IPFS Host</Form.Label>
381+
<Form.Control type="text" placeholder="e.g., ipfs.infura.io" value={ipfsHost} onChange={(e) => setIpfsHost(e.target.value)} />
382+
</Form.Group>
383+
<Form.Group className="mb-2" controlId="formIpfsPort">
384+
<Form.Label className="text-uppercase mb-0">IPFS Port</Form.Label>
385+
<Form.Control type="text" placeholder="e.g., 5001" value={ipfsPort} onChange={(e) => setIpfsPort(e.target.value)} />
386+
</Form.Group>
387+
<Form.Group className="mb-2" controlId="formIpfsProtocol">
388+
<Form.Label className="text-uppercase mb-0">IPFS Protocol</Form.Label>
389+
<Form.Control type="text" placeholder="e.g., https" value={ipfsProtocol} onChange={(e) => setIpfsProtocol(e.target.value)} />
390+
</Form.Group>
391+
<Form.Group className="mb-2" controlId="formIpfsProjectId">
392+
<Form.Label className="text-uppercase mb-0">Project ID (Optional)</Form.Label>
393+
<Form.Control type="text" placeholder="Infura Project ID" value={ipfsProjectId} onChange={(e) => setIpfsProjectId(e.target.value)} />
394+
</Form.Group>
395+
<Form.Group className="mb-2" controlId="formIpfsProjectSecret">
396+
<Form.Label className="text-uppercase mb-0">Project Secret (Optional)</Form.Label>
397+
<Form.Control type="password" placeholder="Infura Project Secret" value={ipfsProjectSecret} onChange={(e) => setIpfsProjectSecret(e.target.value)} />
398+
</Form.Group>
399+
</>
400+
)}
401+
<hr />
402+
403+
        <Button
404+
          data-id="deployDapp-IPFS"
405+
          variant="primary"
406+
          type="button"
407+
          className="mt-3 w-100"
408+
          onClick={handleIpfsDeploy}
409+
disabled={isDeploying || (showIpfsSettings && !ipfsHost)}
410+
        >
411+
{isDeploying ? (
412+
<><i className="fas fa-spinner fa-spin me-1"></i> <FormattedMessage id="quickDapp.deploying" defaultMessage="Deploying..." /></>
413+
) : (
414+
<FormattedMessage id="quickDapp.deployToIPFS" defaultMessage="Deploy to IPFS" />
415+
)}
416+
        </Button>
417+
418+
{deployResult.cid && (
419+
<Alert variant="success" className="mt-3 small" style={{ wordBreak: 'break-all' }}>
420+
<div className="fw-bold">Deployed Successfully!</div>
421+
<div><strong>CID:</strong> {deployResult.cid}</div>
422+
<hr className="my-2" />
423+
<a href={`https://gateway.ipfs.io/ipfs/${deployResult.cid}`} target="_blank" rel="noopener noreferrer">
424+
View on ipfs.io Gateway
425+
</a>
426+
</Alert>
427+
)}
428+
{deployResult.error && (
429+
<Alert variant="danger" className="mt-3 small">
430+
{deployResult.error}
431+
</Alert>
432+
)}
433+
434+
      </Form>
199435
</div>
200436
);
201437
}

apps/quick-dapp/src/components/EditHtmlTemplate/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface ParsedPagesResult {
1616
pages: Pages
1717
}
1818

19-
const readDappFiles = async (path: string, map: Map<string, string>) => {
19+
export const readDappFiles = async (path: string, map: Map<string, string>) => {
2020
try {
2121
const files = await remixClient.call('fileManager', 'readdir', path);
2222

0 commit comments

Comments
 (0)