Skip to content

Commit ebc85d7

Browse files
committed
feat: multi-job PDA + visual escrow flow UI
On-chain: - Add job_id nonce to PDA seed β€” unlimited jobs per wallet - PDA seeds: [job, poster, &job_id.to_le_bytes()] - Redeployed to devnet UI: - Visual escrow flow: Poster β†’ PDA Escrow β†’ Worker - Live SOL balances for wallet, PDA, and worker - New Job button (random nonce) to create fresh jobs - Job ID display in controls panel - Glassmorphic escrow cards with animated arrows - refreshEscrowBalances on init, lock_stake, settle
1 parent e4d58f9 commit ebc85d7

3 files changed

Lines changed: 277 additions & 12 deletions

File tree

β€Žclient/src/App.cssβ€Ž

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,4 +718,193 @@ code {
718718
.event-stream {
719719
max-height: 260px;
720720
}
721+
722+
.escrow-flow {
723+
flex-direction: column;
724+
gap: 0.6rem;
725+
}
726+
727+
.escrow-arrow {
728+
transform: rotate(90deg);
729+
width: 30px;
730+
height: 20px;
731+
align-self: center;
732+
}
733+
}
734+
735+
/* ---- Escrow Visualization ---- */
736+
.escrow-section {
737+
margin-bottom: 1.5rem;
738+
}
739+
740+
.section-heading {
741+
font-size: 0.9rem;
742+
font-weight: 600;
743+
color: var(--text-dim);
744+
margin-bottom: 0.8rem;
745+
text-transform: uppercase;
746+
letter-spacing: 0.06em;
747+
}
748+
749+
.escrow-flow {
750+
display: flex;
751+
align-items: center;
752+
justify-content: center;
753+
gap: 0;
754+
padding: 1.2rem;
755+
background: var(--bg-glass);
756+
border: 1px solid var(--border);
757+
border-radius: 16px;
758+
backdrop-filter: blur(12px);
759+
}
760+
761+
.escrow-account {
762+
display: flex;
763+
flex-direction: column;
764+
align-items: center;
765+
padding: 1rem 1.5rem;
766+
border-radius: 12px;
767+
border: 1px solid var(--border);
768+
background: rgba(10, 18, 28, 0.6);
769+
min-width: 130px;
770+
transition: all 0.4s ease;
771+
}
772+
773+
.escrow-account.escrow-active {
774+
border-color: var(--accent);
775+
box-shadow: 0 0 20px rgba(52, 211, 153, 0.15), 0 0 40px rgba(52, 211, 153, 0.05);
776+
transform: scale(1.04);
777+
}
778+
779+
.escrow-account.escrow-pda {
780+
border-color: var(--er-color);
781+
background: rgba(251, 191, 36, 0.04);
782+
}
783+
784+
.escrow-account.escrow-pda.escrow-active {
785+
border-color: var(--er-color);
786+
box-shadow: 0 0 20px rgba(251, 191, 36, 0.2), 0 0 40px rgba(251, 191, 36, 0.05);
787+
}
788+
789+
.escrow-icon {
790+
font-size: 1.6rem;
791+
margin-bottom: 0.3rem;
792+
}
793+
794+
.escrow-label {
795+
font-size: 0.75rem;
796+
font-weight: 700;
797+
text-transform: uppercase;
798+
letter-spacing: 0.08em;
799+
color: var(--text-dim);
800+
margin-bottom: 0.2rem;
801+
}
802+
803+
.escrow-balance {
804+
font-family: 'JetBrains Mono', monospace;
805+
font-size: 1.1rem;
806+
font-weight: 700;
807+
color: var(--text);
808+
}
809+
810+
.escrow-balance.accent {
811+
color: var(--accent);
812+
text-shadow: 0 0 12px var(--accent-glow);
813+
}
814+
815+
.escrow-sublabel {
816+
font-size: 0.65rem;
817+
color: var(--text-dim);
818+
margin-top: 0.2rem;
819+
}
820+
821+
.escrow-arrow {
822+
display: flex;
823+
flex-direction: column;
824+
align-items: center;
825+
padding: 0 0.6rem;
826+
opacity: 0.3;
827+
transition: all 0.4s ease;
828+
}
829+
830+
.escrow-arrow.arrow-active {
831+
opacity: 1;
832+
}
833+
834+
.arrow-label {
835+
font-size: 0.6rem;
836+
font-weight: 600;
837+
color: var(--accent);
838+
letter-spacing: 0.05em;
839+
text-transform: uppercase;
840+
margin-bottom: 0.3rem;
841+
}
842+
843+
.arrow-line {
844+
width: 60px;
845+
height: 2px;
846+
background: linear-gradient(90deg, transparent, var(--accent), transparent);
847+
border-radius: 1px;
848+
position: relative;
849+
}
850+
851+
.escrow-arrow.arrow-active .arrow-line {
852+
background: linear-gradient(90deg, var(--accent), var(--er-color), var(--accent));
853+
box-shadow: 0 0 8px var(--accent-glow);
854+
animation: arrowPulse 1.5s ease-in-out infinite;
855+
}
856+
857+
@keyframes arrowPulse {
858+
859+
0%,
860+
100% {
861+
opacity: 0.7;
862+
}
863+
864+
50% {
865+
opacity: 1;
866+
}
867+
}
868+
869+
.escrow-legend {
870+
display: flex;
871+
gap: 1.2rem;
872+
justify-content: center;
873+
margin-top: 0.8rem;
874+
flex-wrap: wrap;
875+
}
876+
877+
.legend-item {
878+
font-size: 0.7rem;
879+
color: var(--text-dim);
880+
display: flex;
881+
align-items: center;
882+
gap: 0.4rem;
883+
}
884+
885+
.legend-item.success {
886+
color: var(--accent);
887+
font-weight: 600;
888+
}
889+
890+
.legend-dot {
891+
width: 6px;
892+
height: 6px;
893+
border-radius: 50%;
894+
display: inline-block;
895+
}
896+
897+
.legend-dot.l1 {
898+
background: var(--l1-color);
899+
box-shadow: 0 0 4px var(--accent-glow);
900+
}
901+
902+
/* New Job button */
903+
.btn-new {
904+
border-color: var(--er-color) !important;
905+
color: var(--er-color) !important;
906+
}
907+
908+
.btn-new:hover {
909+
background: rgba(251, 191, 36, 0.1) !important;
721910
}

β€Žclient/src/App.tsxβ€Ž

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -192,16 +192,21 @@ function App() {
192192
const [activeStep, setActiveStep] = useState<PipelineStep>('idle')
193193
const [running, setRunning] = useState(false)
194194
const [completedSteps, setCompletedSteps] = useState<Set<PipelineStep>>(new Set())
195+
const [escrowBal, setEscrowBal] = useState<string>('β€”')
196+
const [workerBal, setWorkerBal] = useState<string>('β€”')
197+
const [jobId, setJobId] = useState<number>(() => Math.floor(Math.random() * 2 ** 32))
195198
const eventIdRef = useRef(0)
196199
const eventsEndRef = useRef<HTMLDivElement>(null)
197200

198201
const jobPDA = useMemo(() => {
199202
if (!publicKey) return null
203+
const idBuf = Buffer.alloc(8)
204+
idBuf.writeBigUInt64LE(BigInt(jobId))
200205
return PublicKey.findProgramAddressSync(
201-
[Buffer.from('job'), publicKey.toBuffer()],
206+
[Buffer.from('job'), publicKey.toBuffer(), idBuf],
202207
PROGRAM_ID
203208
)[0]
204-
}, [publicKey])
209+
}, [publicKey, jobId])
205210

206211
const archivePDA = useMemo(() => {
207212
if (!jobPDA) return null
@@ -211,6 +216,18 @@ function App() {
211216
)[0]
212217
}, [jobPDA])
213218

219+
const refreshEscrowBalances = useCallback(async () => {
220+
if (!jobPDA) return
221+
try {
222+
const pdaLamports = await connection.getBalance(jobPDA)
223+
setEscrowBal((pdaLamports / LAMPORTS_PER_SOL).toFixed(4))
224+
} catch { setEscrowBal('β€”') }
225+
try {
226+
const burnerLamports = await connection.getBalance(erBurner.publicKey)
227+
setWorkerBal((burnerLamports / LAMPORTS_PER_SOL).toFixed(4))
228+
} catch { setWorkerBal('β€”') }
229+
}, [jobPDA, connection])
230+
214231
const addEvent = useCallback((label: string, type: EventEntry['type'], extra?: Partial<EventEntry>) => {
215232
const entry: EventEntry = {
216233
id: ++eventIdRef.current,
@@ -325,6 +342,7 @@ function App() {
325342

326343
const tx = await program.methods
327344
.initializeJob(
345+
new anchor.BN(jobId),
328346
new anchor.BN(0.1 * LAMPORTS_PER_SOL),
329347
new anchor.BN(Math.floor(Date.now() / 1000) + 3600),
330348
randomHash()
@@ -333,7 +351,8 @@ function App() {
333351
.transaction()
334352

335353
const sig = await sendTx(connection, tx)
336-
addEvent(`Job created on L1`, 'success', { txHash: sig, ms: Date.now() - start })
354+
addEvent(`Job #${jobId} created β€” 0.1 SOL escrowed`, 'success', { txHash: sig, ms: Date.now() - start })
355+
await refreshEscrowBalances()
337356
setCompletedSteps(prev => new Set([...prev, 'init']))
338357
return true
339358
} catch (e) {
@@ -549,6 +568,7 @@ function App() {
549568

550569
const sig = await sendL1WithBurner(tx)
551570
addEvent(`πŸ’Ž Stake locked in escrow`, 'success', { txHash: sig, ms: Date.now() - start })
571+
await refreshEscrowBalances()
552572
setCompletedSteps(prev => new Set([...prev, 'staking']))
553573
return true
554574
} catch (e) {
@@ -577,6 +597,8 @@ function App() {
577597

578598
const sig = await sendTx(connection, tx)
579599
addEvent(`βœ… Job PASSED β€” SOL transferred to worker`, 'success', { txHash: sig, ms: Date.now() - start })
600+
await refreshEscrowBalances()
601+
await refreshBalance()
580602
setCompletedSteps(prev => new Set([...prev, 'settling']))
581603
return true
582604
} catch (e) {
@@ -625,7 +647,7 @@ function App() {
625647
if (!delegateResult) { setRunning(false); return }
626648
await new Promise(r => setTimeout(r, 1000))
627649

628-
const job = await program.account.job.fetch(jobPDA)
650+
const job = await (program.account as any).job.fetch(jobPDA)
629651
const status = job.status as number
630652
let erEndpoint: string | null = null
631653

@@ -758,7 +780,7 @@ function App() {
758780
</div>
759781
</div>
760782
<p className="tagline">
761-
Real-time Bounty Pipeline Β· Solana L1 ↔ MagicBlock ER
783+
Trustless Bounty Pipeline Β· Escrow-Protected Β· Solana L1 ↔ MagicBlock ER
762784
</p>
763785
</header>
764786

@@ -799,6 +821,43 @@ function App() {
799821
</div>
800822
</section>
801823

824+
{/* Escrow Visualization */}
825+
<section className="escrow-section">
826+
<h3 className="section-heading">πŸ’° Escrow Flow</h3>
827+
<div className="escrow-flow">
828+
<div className={`escrow-account ${activeStep === 'init' ? 'escrow-active' : ''}`}>
829+
<div className="escrow-icon">πŸ‘€</div>
830+
<div className="escrow-label">Poster</div>
831+
<div className="escrow-balance">{balanceSol} SOL</div>
832+
<div className="escrow-sublabel">Pays reward</div>
833+
</div>
834+
<div className={`escrow-arrow ${completedSteps.has('init') ? 'arrow-active' : ''}`}>
835+
<span className="arrow-label">reward β†’</span>
836+
<div className="arrow-line" />
837+
</div>
838+
<div className={`escrow-account escrow-pda ${(activeStep === 'staking' || activeStep === 'settling') ? 'escrow-active' : ''}`}>
839+
<div className="escrow-icon">πŸ”</div>
840+
<div className="escrow-label">Job PDA</div>
841+
<div className="escrow-balance accent">{escrowBal} SOL</div>
842+
<div className="escrow-sublabel">Escrow vault</div>
843+
</div>
844+
<div className={`escrow-arrow ${completedSteps.has('settling') ? 'arrow-active' : ''}`}>
845+
<span className="arrow-label">β†’ payout</span>
846+
<div className="arrow-line" />
847+
</div>
848+
<div className={`escrow-account ${activeStep === 'staking' ? 'escrow-active' : ''}`}>
849+
<div className="escrow-icon">πŸ€–</div>
850+
<div className="escrow-label">Worker</div>
851+
<div className="escrow-balance">{workerBal} SOL</div>
852+
<div className="escrow-sublabel">Stakes & earns</div>
853+
</div>
854+
</div>
855+
<div className="escrow-legend">
856+
<span className="legend-item"><span className="legend-dot l1" />On-chain escrow protects both parties</span>
857+
{activeStep === 'complete' && <span className="legend-item success">βœ… All SOL transfers verified on L1</span>}
858+
</div>
859+
</section>
860+
802861
{/* Controls + Event Stream */}
803862
<div className="bottom-row">
804863
{/* Controls */}
@@ -819,27 +878,38 @@ function App() {
819878
<span className="kv-label">balance</span>
820879
<span className="kv-val accent">{balanceSol} SOL</span>
821880
</div>
881+
<div className="kv">
882+
<span className="kv-label">job ID</span>
883+
<code className="kv-val dim">#{jobId}</code>
884+
</div>
822885
<div className="kv">
823886
<span className="kv-label">job PDA</span>
824887
<code className="kv-val dim">{jobPDA?.toBase58().slice(0, 12)}...</code>
825888
</div>
889+
<div className="kv">
890+
<span className="kv-label">escrow</span>
891+
<span className="kv-val accent">{escrowBal} SOL</span>
892+
</div>
826893
</div>
827894

828895
<div className="btn-group">
829896
<button className="btn btn-run" onClick={runFullDemo} disabled={running}>
830897
{running ? '⏳ Running...' : 'β–Ά Run Full Lifecycle'}
831898
</button>
899+
<button className="btn btn-sm btn-new" onClick={() => { setJobId(Math.floor(Math.random() * 2 ** 32)); setEvents([]); setCompletedSteps(new Set()); setActiveStep('idle') }} disabled={running}>
900+
πŸ†• New Job
901+
</button>
832902
<button className="btn btn-sm" onClick={stepAirdrop} disabled={running}>
833903
πŸ’§ Airdrop
834904
</button>
835-
<button className="btn btn-sm" onClick={refreshBalance} disabled={running}>
905+
<button className="btn btn-sm" onClick={() => { refreshBalance(); refreshEscrowBalances() }} disabled={running}>
836906
πŸ”„ Refresh
837907
</button>
838908
</div>
839909

840910
{activeStep === 'complete' && (
841911
<div className="complete-banner">
842-
πŸŽ‰ Pipeline Complete β€” All 7 steps executed successfully
912+
πŸŽ‰ Pipeline Complete β€” All {PIPELINE_ORDER.length - 2} steps with real SOL escrow
843913
</div>
844914
)}
845915
</>

0 commit comments

Comments
Β (0)