-
setCurrentStep(Math.max(1, currentStep - 1))}
- disabled={currentStep === 1}
- style={{
- padding: '10px 16px',
- background:
- currentStep === 1 ? 'var(--bg-elevated)' : 'var(--bg-elevated)',
- color:
- currentStep === 1
- ? 'var(--text-muted)'
- : 'var(--text-primary)',
- border: '1px solid var(--border-bright)',
- borderRadius: 'var(--radius-md)',
- fontWeight: 600,
- fontSize: '12px',
- cursor: currentStep === 1 ? 'not-allowed' : 'pointer',
- opacity: currentStep === 1 ? 0.5 : 1,
- transition: 'var(--transition)',
- }}
- >
- ← Previous
-
-
-
- {currentStep === 1 && (
+
+ {STEPS.map((step, index) => (
+
setCurrentStep(2)}
- disabled={!wasmFile}
- style={{
- padding: '10px 16px',
- background: wasmFile ? 'var(--cyan)' : 'var(--bg-elevated)',
- color: wasmFile ? 'var(--bg-base)' : 'var(--text-muted)',
- border: 'none',
- borderRadius: 'var(--radius-md)',
- fontWeight: 600,
- fontSize: '12px',
- cursor: wasmFile ? 'pointer' : 'not-allowed',
- opacity: wasmFile ? 1 : 0.5,
- transition: 'var(--transition)',
- }}
+ type="button"
+ onClick={() => step.id < currentStep && setCurrentStep(step.id)}
+ disabled={step.id > currentStep}
+ style={stepButtonStyle(step, currentStep)}
+ title={step.label}
>
- Next →
+ {step.icon}
- )}
+
+
{step.label}
+
{step.description}
+
+ {index < STEPS.length - 1 && (
+
+ )}
+
+ ))}
+
- {currentStep === 2 && (
-
setCurrentStep(3)}
- disabled={!canProceed}
- style={{
- padding: '10px 16px',
- background: canProceed ? 'var(--cyan)' : 'var(--bg-elevated)',
- color: canProceed ? 'var(--bg-base)' : 'var(--text-muted)',
- border: 'none',
- borderRadius: 'var(--radius-md)',
- fontWeight: 600,
- fontSize: '12px',
- cursor: canProceed ? 'pointer' : 'not-allowed',
- opacity: canProceed ? 1 : 0.5,
- transition: 'var(--transition)',
- }}
- >
- Next →
-
- )}
+
+ {renderStepBody()}
- {currentStep === 3 && (
+
+
+ Reset
+
+
+
setCurrentStep((step) => Math.max(1, step - 1))}
+ disabled={!canGoBack || isLoading}
+ style={secondaryActionStyle(!canGoBack || isLoading)}
>
- {isLoading ? 'Estimating...' : 'Estimate & Continue →'}
+ Back
- )}
- {currentStep === 4 && (
- <>
+ {currentStep === 3 ? (
+
+ {isLoading ? 'Estimating...' : 'Estimate fees'}
+
+ ) : currentStep === 4 ? null : (
{
+ if (currentStep === 1 && wasmFile) {
+ setCurrentStep(2);
+ } else if (currentStep === 2 && !constructorError) {
+ setCurrentStep(3);
+ }
}}
+ disabled={!canGoForward || isLoading}
+ style={primaryActionStyle(!canGoForward || isLoading)}
>
- {isLoading ? 'Deploying...' : '🚀 Deploy to Testnet'}
+ Next
- >
- )}
-
- {currentStep === 5 && (
-
- Start Over
-
- )}
+ )}
+
+
+
+ );
+}
+
+const panelStyle = {
+ background: 'var(--bg-card)',
+ border: '1px solid var(--border)',
+ borderRadius: 'var(--radius-lg)',
+ padding: '24px',
+ minHeight: '420px',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '16px',
+};
+
+const stepperStyle = {
+ display: 'flex',
+ gap: '8px',
+ overflow: 'auto',
+ paddingBottom: '8px',
+};
+
+const summaryGridStyle = {
+ display: 'grid',
+ gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
+ gap: '12px',
+};
+
+const costGridStyle = {
+ display: 'grid',
+ gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))',
+ gap: '8px',
+ fontSize: '11px',
+ fontFamily: 'var(--font-mono)',
+};
+
+const costPanelStyle = {
+ padding: '16px',
+ background: 'rgba(34, 211, 238, 0.08)',
+ border: '1px solid var(--cyan)',
+ borderRadius: 'var(--radius-md)',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '8px',
+};
+
+const summaryBoxStyle = {
+ padding: '16px',
+ background: 'var(--bg-elevated)',
+ borderRadius: 'var(--radius-md)',
+ border: '1px solid var(--border)',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '8px',
+};
+
+const footerStyle = {
+ display: 'flex',
+ justifyContent: 'space-between',
+ gap: '12px',
+ alignItems: 'center',
+ flexWrap: 'wrap',
+ paddingTop: '8px',
+ borderTop: '1px solid var(--border)',
+};
+
+const hintStyle = {
+ padding: '12px',
+ background: 'rgba(255, 184, 0, 0.08)',
+ border: '1px solid var(--amber)',
+ borderRadius: 'var(--radius-md)',
+ fontSize: '12px',
+ color: 'var(--text-secondary)',
+ lineHeight: 1.6,
+};
+
+const alertStyle = {
+ padding: '12px',
+ background: 'rgba(220, 38, 38, 0.1)',
+ border: '1px solid var(--red)',
+ borderRadius: 'var(--radius-md)',
+ fontSize: '12px',
+ lineHeight: 1.5,
+};
+
+function primaryActionStyle(disabled = false) {
+ return {
+ padding: '10px 16px',
+ background: disabled ? 'var(--bg-elevated)' : 'var(--cyan)',
+ color: disabled ? 'var(--text-muted)' : 'var(--bg-base)',
+ border: disabled ? '1px solid var(--border)' : 'none',
+ borderRadius: 'var(--radius-md)',
+ fontFamily: 'var(--font-mono)',
+ fontWeight: 700,
+ fontSize: '12px',
+ cursor: disabled ? 'not-allowed' : 'pointer',
+ transition: 'var(--transition)',
+ };
+}
+
+function secondaryActionStyle(disabled = false) {
+ return {
+ padding: '10px 16px',
+ background: 'var(--bg-elevated)',
+ color: disabled ? 'var(--text-muted)' : 'var(--text-primary)',
+ border: '1px solid var(--border-bright)',
+ borderRadius: 'var(--radius-md)',
+ fontFamily: 'var(--font-mono)',
+ fontWeight: 700,
+ fontSize: '12px',
+ cursor: disabled ? 'not-allowed' : 'pointer',
+ transition: 'var(--transition)',
+ };
+}
+
+function SummaryCard({ label, value }) {
+ return (
+
+
+ {label}
+
+
+ {value}
+
);
}
+
+function LinkCard({ href, title, subtitle, tone = 'primary' }) {
+ const styles = tone === 'primary'
+ ? {
+ background: 'var(--cyan)',
+ color: 'var(--bg-base)',
+ border: 'none',
+ }
+ : {
+ background: 'var(--bg-elevated)',
+ color: 'var(--text-primary)',
+ border: '1px solid var(--border-bright)',
+ };
+
+ return (
+
+ {title}
+ {subtitle}
+
+ );
+}
diff --git a/src/components/deployment/DeploymentTracker.jsx b/src/components/deployment/DeploymentTracker.jsx
index 49976186..c46d956c 100644
--- a/src/components/deployment/DeploymentTracker.jsx
+++ b/src/components/deployment/DeploymentTracker.jsx
@@ -1,5 +1,9 @@
import React from 'react';
+function copyableValue(value) {
+ return value || '—';
+}
+
export default function DeploymentTracker({ status }) {
if (!status) {
return (
@@ -19,10 +23,16 @@ export default function DeploymentTracker({ status }) {
);
}
+ const receipt = status.receipt || status;
+ const timeline = receipt.statusHistory || status.statusHistory || [];
+ const currentState = status.status || receipt.status || 'pending';
+ const isSimulation = Boolean(status.isSimulation || receipt.isSimulation);
+
const getStatusColor = (stat) => {
switch (stat) {
case 'submitted':
case 'confirmed':
+ case 'complete':
return 'var(--green)';
case 'pending':
return 'var(--amber)';
@@ -37,6 +47,7 @@ export default function DeploymentTracker({ status }) {
switch (stat) {
case 'submitted':
case 'confirmed':
+ case 'complete':
return '✅';
case 'pending':
return '⏳';
@@ -52,242 +63,166 @@ export default function DeploymentTracker({ status }) {
style={{
display: 'flex',
flexDirection: 'column',
- gap: '12px',
- padding: '16px',
+ gap: '16px',
+ padding: '18px',
background: 'var(--bg-elevated)',
- borderRadius: 'var(--radius-md)',
- border: `1px solid ${getStatusColor(status.status)}`,
+ borderRadius: 'var(--radius-lg)',
+ border: `1px solid ${getStatusColor(currentState)}`,
}}
>
-
-
{getStatusIcon(status.status)}
-
- {status.status}
-
- {status.isSimulation && (
-
- SIMULATION
-
- )}
+
+
{getStatusIcon(currentState)}
+
+
+
+ {currentState}
+
+ {isSimulation && (
+
+ SIMULATION
+
+ )}
+
+
+ {receipt.networkUsed || status.networkUsed || 'testnet'} deployment receipt
+
+
- {status.contractId && (
-
-
- Contract ID
-
-
- {status.contractId}
-
-
+ {receipt.artifactName && (
+
)}
-
- {status.txHash && (
-
-
- Transaction Hash
-
-
- {status.txHash}
-
-
+ {receipt.contractId &&
}
+ {receipt.txHash &&
}
+ {receipt.sourceAccount &&
}
+ {typeof receipt.constructorArgsCount === 'number' && (
+
)}
-
- {status.sourceAccount && (
-
-
- Source Account
-
-
- {status.sourceAccount}
-
-
+ {receipt.sizeBytes ? (
+
+ ) : null}
+ {receipt.artifactHash && (
+
)}
-
- {typeof status.constructorArgsCount === 'number' && (
-
-
- Constructor Args
-
-
- {status.constructorArgsCount}
-
-
+ {receipt.timestamp && (
+
)}
+
- {status.networkUsed && (
-
-
- Network
-
-
- {status.networkUsed}
-
+ {timeline.length > 0 && (
+
+
+ Deployment Tracking
- )}
+
+ {timeline.map((entry, index) => (
+
+
+
+
+
+ {entry.label}
+
+
+ {entry.status}
+
+
+ {entry.detail && (
+
+ {entry.detail}
+
+ )}
+
+ {new Date(entry.timestamp).toLocaleString()}
+
+
+
+ ))}
+
+
+ )}
- {status.timestamp && (
+ {receipt.estimatedCost && (
+
+
Receipt Summary
-
- Timestamp
-
-
- {new Date(status.timestamp).toLocaleString()}
-
+
Estimated Fee: {receipt.estimatedCost.estimatedFeeStroops.toLocaleString()} stroops
+
Footprint: {receipt.estimatedCost.footprintKb} KB
+
Receipt ID: {copyableValue(receipt.receiptId)}
+
Network: {receipt.networkUsed}
- )}
-
+
+ )}
- {status.error && (
+ {(receipt.error || status.error) && (
- {status.error}
+ {receipt.error || status.error}
+
+ )}
+
+ {(receipt.explorerUrls?.contract || receipt.explorerUrls?.transaction) && (
+
)}
@@ -314,10 +291,50 @@ export default function DeploymentTracker({ status }) {
color: 'var(--text-secondary)',
overflowX: 'auto',
fontFamily: 'var(--font-mono)',
+ lineHeight: 1.5,
}}
>
- {JSON.stringify(status, null, 2)}
+ {JSON.stringify(receipt, null, 2)}
);
}
+
+function InfoCard({ label, value, accent, mono = false }) {
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+ );
+}
diff --git a/src/components/deployment/WASMUploader.jsx b/src/components/deployment/WASMUploader.jsx
index 048043e1..26b49366 100644
--- a/src/components/deployment/WASMUploader.jsx
+++ b/src/components/deployment/WASMUploader.jsx
@@ -1,93 +1,194 @@
-import React, { useState } from 'react';
-import { WASMProcessor } from '../../lib/deployment/WASMProcessor';
+import React, { useMemo, useRef, useState } from 'react';
+import { MAX_WASM_BYTES, WASMProcessor } from '../../lib/deployment/WASMProcessor';
-export default function WASMUploader({ onFile, onError, file }) {
+const DEFAULT_MAX_BYTES = MAX_WASM_BYTES;
+
+export default function WASMUploader({
+ onFile,
+ onError,
+ file,
+ maxSizeBytes = DEFAULT_MAX_BYTES,
+}) {
+ const inputRef = useRef(null);
const [isLoading, setIsLoading] = useState(false);
+ const [isDragging, setIsDragging] = useState(false);
const [localError, setLocalError] = useState(null);
- const handleFileChange = async (e) => {
- const selectedFile = e.target.files?.[0] || null;
- setLocalError(null);
+ const maxSizeMb = useMemo(() => (maxSizeBytes / (1024 * 1024)).toFixed(0), [maxSizeBytes]);
+
+ const emitError = (message) => {
+ setLocalError(message);
+ onError?.(message);
+ onFile?.(null);
+ };
+ const processFile = async (selectedFile) => {
if (!selectedFile) {
onFile?.(null);
return;
}
setIsLoading(true);
+ setLocalError(null);
+
try {
- const bytes = await WASMProcessor.parseFile(selectedFile);
- const sizeKb = Math.ceil(bytes.length / 1024);
-
- if (sizeKb > 256) {
- throw new Error('WASM file is too large (max 256 KB)');
+ if (!selectedFile.name.endsWith('.wasm') && selectedFile.type !== 'application/wasm') {
+ throw new Error('Please select a compiled Soroban WASM file (.wasm)');
+ }
+
+ const metadata = await WASMProcessor.inspectFile(selectedFile);
+
+ if (metadata.sizeBytes > maxSizeBytes) {
+ throw new Error(
+ `WASM file is too large (${metadata.sizeMb.toFixed(2)} MB). The helper supports files up to ${maxSizeMb} MB.`
+ );
}
onFile?.({
file: selectedFile,
- bytes,
- sizeKb,
+ bytes: metadata.bytes,
+ sizeBytes: metadata.sizeBytes,
+ sizeKb: metadata.sizeKb,
+ sizeMb: metadata.sizeMb,
+ checksum: metadata.artifactHash,
+ mimeType: metadata.mimeType,
+ lastModified: metadata.lastModified,
});
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Failed to process WASM file';
- setLocalError(errorMsg);
- onError?.(errorMsg);
- onFile?.(null);
+ emitError(errorMsg);
} finally {
setIsLoading(false);
}
};
- const displayError = localError || (onError && typeof onError === 'string' ? onError : null);
+ const handleFileChange = async (event) => {
+ const selectedFile = event.target.files?.[0] || null;
+ await processFile(selectedFile);
+ };
+
+ const handleDrop = async (event) => {
+ event.preventDefault();
+ setIsDragging(false);
+ const selectedFile = event.dataTransfer.files?.[0] || null;
+ await processFile(selectedFile);
+ };
+
+ const displayError = localError || null;
return (
-
inputRef.current?.click()}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ inputRef.current?.click();
+ }
+ }}
+ onDragEnter={() => setIsDragging(true)}
+ onDragOver={(event) => {
+ event.preventDefault();
+ setIsDragging(true);
+ }}
+ onDragLeave={() => setIsDragging(false)}
+ onDrop={handleDrop}
style={{
display: 'flex',
flexDirection: 'column',
- gap: '8px',
- padding: '20px',
- border: `2px dashed ${displayError ? 'var(--red)' : file ? 'var(--green)' : 'var(--border-bright)'}`,
- borderRadius: 'var(--radius-md)',
- background: displayError ? 'rgba(220, 38, 38, 0.08)' : file ? 'rgba(34, 197, 94, 0.08)' : 'var(--bg-elevated)',
+ gap: '10px',
+ padding: '22px',
+ border: `2px dashed ${
+ displayError ? 'var(--red)' : file ? 'var(--green)' : isDragging ? 'var(--cyan)' : 'var(--border-bright)'
+ }`,
+ borderRadius: 'var(--radius-lg)',
+ background: displayError
+ ? 'rgba(220, 38, 38, 0.08)'
+ : file
+ ? 'rgba(34, 197, 94, 0.08)'
+ : isDragging
+ ? 'rgba(34, 211, 238, 0.08)'
+ : 'var(--bg-elevated)',
cursor: isLoading ? 'not-allowed' : 'pointer',
transition: 'all var(--transition)',
- opacity: isLoading ? 0.6 : 1,
+ opacity: isLoading ? 0.75 : 1,
alignItems: 'center',
textAlign: 'center',
+ minHeight: '180px',
+ justifyContent: 'center',
}}
>
- 📦
-
- {file ? `✓ ${file.file.name}` : isLoading ? 'Processing...' : 'Drop WASM file or click to select'}
+
📦
+
+
+ {file
+ ? `Uploaded ${file.file.name}`
+ : isLoading
+ ? 'Inspecting WASM...'
+ : 'Drop a WASM file here or click to browse'}
+
+
+ Supports large Soroban artifacts up to {maxSizeMb} MB
+
+
{file && (
- {file.sizeKb} KB
+
+ Size
+
+ {file.sizeMb.toFixed(2)} MB
+
+
+
+ Checksum
+
+ {file.checksum.slice(0, 16)}…
+
+
+
+ Type
+
+ {file.mimeType}
+
+
)}
+
-
+
+
{displayError && (
;
+ estimatedCost: CostEstimate;
+ explorerUrls: {
+ contract?: string;
+ transaction?: string;
+ };
+ statusHistory: DeploymentTimelineEntry[];
+ timestamp: number;
+ error?: string;
+}
+
export interface DeploymentResult {
status: 'pending' | 'submitted' | 'confirmed' | 'failed';
sourceAccount: string;
@@ -16,72 +65,205 @@ export interface DeploymentResult {
timestamp?: number;
networkUsed: 'testnet' | 'mainnet';
isSimulation: boolean;
+ receipt?: DeploymentReceipt;
}
export class ContractDeployer {
async deployContract(
wasmBytes: Uint8Array,
- constructorArgs: any[],
+ constructorArgs: ConstructorArgInput[],
sourceAccount: string,
- network: 'testnet' | 'mainnet' = 'testnet'
+ network: 'testnet' | 'mainnet' = 'testnet',
+ artifactMeta: Partial
> = {}
): Promise {
+ const normalizedArgs = this.normalizeConstructorArgs(constructorArgs);
+ const wasmHash = await WASMProcessor.hashBytes(wasmBytes);
+ const contractId = this.generateContractId(wasmHash);
+ const txHash = this.generateTransactionHash(wasmHash, sourceAccount, network);
+ const estimatedCost = await this.estimateDeploymentCost(wasmBytes, normalizedArgs);
+ const receipt = this.buildReceipt({
+ wasmBytes,
+ wasmHash,
+ contractId,
+ txHash,
+ sourceAccount,
+ network,
+ normalizedArgs,
+ estimatedCost,
+ isSimulation: network === 'mainnet',
+ artifactMeta,
+ });
+
// For mainnet, only allow simulation
if (network === 'mainnet') {
return {
status: 'pending',
sourceAccount,
- constructorArgsCount: constructorArgs.length,
+ contractId,
+ txHash,
+ constructorArgsCount: normalizedArgs.length,
+ timestamp: Date.now(),
networkUsed: 'mainnet',
isSimulation: true,
error: 'Mainnet: simulation only. Actual deployment requires UI confirmation on testnet.',
+ receipt,
};
}
- // Generate deterministic contract ID based on WASM hash
- const contractId = this.generateContractId(wasmBytes);
-
return {
status: 'submitted',
sourceAccount,
contractId,
- constructorArgsCount: constructorArgs.length,
- txHash: `tx_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
+ constructorArgsCount: normalizedArgs.length,
+ txHash,
timestamp: Date.now(),
networkUsed: network,
isSimulation: false,
+ receipt,
};
}
async estimateDeploymentCost(
wasmBytes: Uint8Array,
- constructorArgs: any[]
+ constructorArgs: ConstructorArgInput[]
): Promise {
- const { CostEstimator } = await import('./CostEstimator');
return CostEstimator.estimate(wasmBytes, constructorArgs);
}
- private generateContractId(wasmBytes: Uint8Array): string {
- // Generate a contract ID prefix (in real Soroban this would be done by the network)
- const hash = Array.from(wasmBytes)
- .slice(0, 16)
- .map((b) => b.toString(16).padStart(2, '0'))
- .join('')
- .slice(0, 16)
- .toUpperCase();
- return `C${hash}`;
+ private normalizeConstructorArgs(constructorArgs: ConstructorArgInput[]) {
+ return (constructorArgs || [])
+ .filter((arg) => arg && String(arg.value ?? '').trim() !== '')
+ .map((arg) => ({
+ name: arg.name?.trim() || undefined,
+ type: arg.type || 'string',
+ value: String(arg.value ?? ''),
+ encodedValue: this.encodeConstructorArg(arg),
+ }));
+ }
+
+ private encodeConstructorArg(arg: ConstructorArgInput): string {
+ const value = String(arg.value ?? '');
+ switch (arg.type) {
+ case 'int':
+ return `i128:${value}`;
+ case 'bool':
+ return `bool:${value === 'true' || value === '1'}`;
+ case 'address':
+ return `address:${value}`;
+ case 'bytes':
+ return `bytes:${value.replace(/^0x/, '').toLowerCase()}`;
+ default:
+ return `string:${value}`;
+ }
+ }
+
+ private generateContractId(wasmHash: string): string {
+ const normalized = (wasmHash || '0').replace(/[^0-9a-f]/gi, '').toUpperCase();
+ const padded = normalized.length >= 16 ? normalized : normalized.padEnd(16, '0');
+ return `C${padded.slice(0, 16)}`;
+ }
+
+ private generateTransactionHash(wasmHash: string, sourceAccount: string, network: 'testnet' | 'mainnet') {
+ const seed = `${wasmHash}:${sourceAccount}:${network}`;
+ let hash = 0;
+ for (const char of seed) {
+ hash = Math.imul(hash ^ char.charCodeAt(0), 2654435761) >>> 0;
+ }
+ return `tx_${hash.toString(16).padStart(8, '0')}_${Date.now().toString(16)}`;
+ }
+
+ private buildReceipt({
+ wasmBytes,
+ wasmHash,
+ contractId,
+ txHash,
+ sourceAccount,
+ network,
+ normalizedArgs,
+ estimatedCost,
+ isSimulation,
+ artifactMeta,
+ }: {
+ wasmBytes: Uint8Array;
+ wasmHash: string;
+ contractId: string;
+ txHash: string;
+ sourceAccount: string;
+ network: 'testnet' | 'mainnet';
+ normalizedArgs: Array<{
+ name?: string;
+ type: ConstructorArgType;
+ value: string;
+ encodedValue: string;
+ }>;
+ estimatedCost: CostEstimate;
+ isSimulation: boolean;
+ artifactMeta: Partial>;
+ }): DeploymentReceipt {
+ const sizeBytes = wasmBytes.length;
+ const timestamp = Date.now();
+ const statusHistory: DeploymentTimelineEntry[] = [
+ {
+ id: 'upload',
+ label: 'WASM uploaded',
+ status: 'complete',
+ detail: `${Math.ceil(sizeBytes / 1024)} KB artifact ready for deployment`,
+ timestamp,
+ },
+ {
+ id: 'encode',
+ label: 'Constructor encoded',
+ status: 'complete',
+ detail: `${normalizedArgs.length} constructor argument(s) normalized`,
+ timestamp,
+ },
+ {
+ id: 'submit',
+ label: isSimulation ? 'Simulation complete' : 'Broadcast submitted',
+ status: isSimulation ? 'complete' : 'pending',
+ detail: isSimulation
+ ? 'Mainnet mode keeps the deployment in simulation only.'
+ : 'Transaction prepared for Soroban RPC submission.',
+ timestamp,
+ },
+ ];
+
+ return {
+ artifactName: artifactMeta.artifactName || 'contract.wasm',
+ sizeBytes: artifactMeta.sizeBytes || sizeBytes,
+ sizeKb: Math.ceil((artifactMeta.sizeBytes || sizeBytes) / 1024),
+ sizeMb: Number(((artifactMeta.sizeBytes || sizeBytes) / (1024 * 1024)).toFixed(2)),
+ mimeType: 'application/wasm',
+ lastModified: artifactMeta.lastModified || timestamp,
+ artifactHash: artifactMeta.artifactHash || wasmHash,
+ receiptId: `rcpt_${wasmHash.slice(0, 12)}`,
+ contractId,
+ txHash,
+ sourceAccount,
+ networkUsed: network,
+ isSimulation,
+ status: isSimulation ? 'pending' : 'submitted',
+ constructorArgsCount: normalizedArgs.length,
+ constructorArgs: normalizedArgs,
+ estimatedCost,
+ explorerUrls: {
+ contract: `https://stellar.expert/explorer/${network === 'mainnet' ? 'public' : 'testnet'}/contract/${contractId}`,
+ transaction: `https://stellar.expert/explorer/${network === 'mainnet' ? 'public' : 'testnet'}/tx/${txHash}`,
+ },
+ statusHistory,
+ timestamp,
+ };
}
async simulateDeployment(
wasmBytes: Uint8Array,
- constructorArgs: any[],
+ constructorArgs: ConstructorArgInput[],
sourceAccount: string,
network: 'testnet' | 'mainnet' = 'testnet'
): Promise {
- // Estimate the cost
- const cost = await this.estimateDeploymentCost(wasmBytes, constructorArgs);
+ const wasmHash = await WASMProcessor.hashBytes(wasmBytes);
+ const contractId = this.generateContractId(wasmHash);
- const contractId = this.generateContractId(wasmBytes);
-
return {
status: 'pending',
sourceAccount,
diff --git a/src/lib/deployment/CostEstimator.ts b/src/lib/deployment/CostEstimator.ts
index ac6ba11b..23237239 100644
--- a/src/lib/deployment/CostEstimator.ts
+++ b/src/lib/deployment/CostEstimator.ts
@@ -22,7 +22,18 @@ export class CostEstimator {
constructorArgs: any[]
): Promise {
const kb = Math.ceil(wasmBytes.length / 1024);
- const validArgCount = constructorArgs.filter(arg => arg && String(arg).trim() !== '').length;
+ const validArgCount = constructorArgs.filter((arg) => {
+ if (!arg) return false;
+ if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean') {
+ return String(arg).trim() !== '';
+ }
+
+ if (typeof arg === 'object' && 'value' in arg) {
+ return String((arg as { value?: unknown }).value ?? '').trim() !== '';
+ }
+
+ return String(arg).trim() !== '';
+ }).length;
const baseStorageFee = CostEstimator.BASE_FEE_STROOPS;
const perKbFee = kb * CostEstimator.PER_KB_FEE_STROOPS;
diff --git a/src/lib/deployment/WASMProcessor.ts b/src/lib/deployment/WASMProcessor.ts
index a38c27c7..4cda7b4c 100644
--- a/src/lib/deployment/WASMProcessor.ts
+++ b/src/lib/deployment/WASMProcessor.ts
@@ -1,17 +1,71 @@
import { xdr } from '@stellar/stellar-sdk';
+export const MAX_WASM_BYTES = 20 * 1024 * 1024;
+
+export interface WasmFileMetadata {
+ name: string;
+ sizeBytes: number;
+ sizeKb: number;
+ sizeMb: number;
+ mimeType: string;
+ lastModified: number;
+ artifactHash: string;
+}
+
export interface ScValType {
type: 'int' | 'bool' | 'string' | 'address' | 'bytes' | 'vec' | 'map';
value: any;
}
export class WASMProcessor {
- static async parseFile(file) {
+ static async parseFile(file: File) {
return new Uint8Array(await file.arrayBuffer());
}
+
+ static async inspectFile(file: File): Promise {
+ const bytes = await WASMProcessor.parseFile(file);
+ const artifactHash = await WASMProcessor.hashBytes(bytes);
+ const sizeBytes = bytes.length;
+
+ return {
+ bytes,
+ name: file.name,
+ sizeBytes,
+ sizeKb: Math.ceil(sizeBytes / 1024),
+ sizeMb: Number((sizeBytes / (1024 * 1024)).toFixed(2)),
+ mimeType: file.type || 'application/wasm',
+ lastModified: file.lastModified || Date.now(),
+ artifactHash,
+ };
+ }
+
+ static async hashBytes(bytes: Uint8Array): Promise {
+ const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
+
+ if (typeof crypto !== 'undefined' && crypto?.subtle?.digest) {
+ const digest = await crypto.subtle.digest('SHA-256', buffer);
+ return Array.from(new Uint8Array(digest))
+ .map((byte) => byte.toString(16).padStart(2, '0'))
+ .join('');
+ }
+
+ // Fallback for environments without Web Crypto. This keeps receipts stable in tests.
+ let hash = 0x811c9dc5;
+ for (const byte of bytes) {
+ hash ^= byte;
+ hash = Math.imul(hash, 0x01000193) >>> 0;
+ }
+ return hash.toString(16).padStart(8, '0');
+ }
+
static toScVal(value, type) {
if (type === 'int') return xdr.ScVal.scvI64(xdr.Int64.fromString(String(value || '0')));
- if (type === 'bool') return xdr.ScVal.scvBool(value === 'true');
+ if (type === 'bool') return xdr.ScVal.scvBool(value === 'true' || value === true);
+ if (type === 'bytes') {
+ const normalized = String(value ?? '').replace(/^0x/, '');
+ const bytePairs = normalized.match(/.{1,2}/g) || [];
+ return xdr.ScVal.scvBytes(Uint8Array.from(bytePairs.map((pair) => parseInt(pair, 16))));
+ }
return xdr.ScVal.scvString(String(value ?? ''));
}
}
diff --git a/src/lib/deployment/tests/ContractDeployer.test.ts b/src/lib/deployment/tests/ContractDeployer.test.ts
index 38d06c38..b3900473 100644
--- a/src/lib/deployment/tests/ContractDeployer.test.ts
+++ b/src/lib/deployment/tests/ContractDeployer.test.ts
@@ -51,6 +51,26 @@ describe('ContractDeployer', () => {
expect(result.txHash).toBeDefined();
expect(result.timestamp).toBeGreaterThan(0);
expect(result.constructorArgsCount).toBe(2);
+ expect(result.receipt).toBeDefined();
+ expect(result.receipt?.artifactName).toBe('contract.wasm');
+ expect(result.receipt?.statusHistory.length).toBeGreaterThan(0);
+ });
+
+ it('supports large WASM artifacts and includes receipt metadata', async () => {
+ const largeWasm = new Uint8Array(6 * 1024 * 1024);
+ largeWasm.fill(0x61);
+
+ const result = await deployer.deployContract(
+ largeWasm,
+ mockArgs,
+ mockSourceAccount,
+ 'testnet',
+ { artifactName: 'large-contract.wasm' }
+ );
+
+ expect(result.receipt?.sizeMb).toBeGreaterThan(5);
+ expect(result.receipt?.artifactName).toBe('large-contract.wasm');
+ expect(result.receipt?.artifactHash).toBeDefined();
});
});
@@ -72,4 +92,4 @@ describe('ContractDeployer', () => {
expect(cost.footprintKb).toBe(45);
});
});
-});
\ No newline at end of file
+});