feat: SPL Token Launcher (TOKEN FORGE) on LAB
This commit is contained in:
parent
7611698889
commit
55c8f499f1
4 changed files with 1467 additions and 0 deletions
|
|
@ -35,6 +35,13 @@
|
|||
box-shadow: 0 4px 20px rgba(20, 241, 149, 0.15);
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
}
|
||||
.deploy-card--amber {
|
||||
border-left-color: rgba(245, 166, 35, 0.6);
|
||||
}
|
||||
.deploy-card--amber:hover {
|
||||
border-left-color: #F5A623;
|
||||
box-shadow: 0 4px 20px rgba(245, 166, 35, 0.15);
|
||||
}
|
||||
.deploy-card-status {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.6rem;
|
||||
|
|
@ -125,6 +132,18 @@
|
|||
</div>
|
||||
<div class="deploy-card-url">jaeswift.xyz/soldomains</div>
|
||||
</a>
|
||||
|
||||
<a href="/tokenlauncher" class="deploy-card deploy-card--amber">
|
||||
<div class="deploy-card-status" style="color: #F5A623;">◆ EXPERIMENTAL</div>
|
||||
<div class="deploy-card-title">TOKEN FORGE</div>
|
||||
<div class="deploy-card-desc">Deploy custom SPL tokens on Solana. Set name, symbol, supply, and metadata — launch in one transaction.</div>
|
||||
<div class="deploy-card-tags">
|
||||
<span class="deploy-tag" style="color: rgba(245, 166, 35, 0.8); background: rgba(245, 166, 35, 0.08); border-color: rgba(245, 166, 35, 0.2);">SOLANA</span>
|
||||
<span class="deploy-tag" style="color: rgba(245, 166, 35, 0.8); background: rgba(245, 166, 35, 0.08); border-color: rgba(245, 166, 35, 0.2);">SPL</span>
|
||||
<span class="deploy-tag" style="color: rgba(245, 166, 35, 0.8); background: rgba(245, 166, 35, 0.08); border-color: rgba(245, 166, 35, 0.2);">TOKENS</span>
|
||||
</div>
|
||||
<div class="deploy-card-url">jaeswift.xyz/tokenlauncher</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
599
css/tokenlauncher.css
Normal file
599
css/tokenlauncher.css
Normal file
|
|
@ -0,0 +1,599 @@
|
|||
/* TOKEN FORGE — SPL Token Launcher */
|
||||
|
||||
:root {
|
||||
--tf-amber: #F5A623;
|
||||
--tf-amber-bright: #FFB800;
|
||||
--tf-amber-dim: rgba(245, 166, 35, 0.6);
|
||||
--tf-amber-glow: rgba(245, 166, 35, 0.15);
|
||||
--tf-amber-border: rgba(245, 166, 35, 0.3);
|
||||
--tf-amber-bg: rgba(245, 166, 35, 0.08);
|
||||
--tf-red: #FF4444;
|
||||
--tf-green: #14F195;
|
||||
}
|
||||
|
||||
.tf-container {
|
||||
max-width: clamp(900px, 80vw, 1400px);
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem 3rem;
|
||||
}
|
||||
|
||||
/* ── Wallet Bar ────────────────────────────────────────────── */
|
||||
|
||||
.tf-wallet-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: rgba(16, 16, 16, 0.6);
|
||||
border: 1px solid var(--tf-amber-border);
|
||||
}
|
||||
|
||||
.tf-wallet-status {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.65rem;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.tf-wallet-status.connected {
|
||||
color: var(--tf-amber);
|
||||
}
|
||||
|
||||
.tf-wallet-btn {
|
||||
background: var(--tf-amber-bg);
|
||||
border: 1px solid var(--tf-amber-border);
|
||||
color: var(--tf-amber);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 2px;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.tf-wallet-btn:hover {
|
||||
background: rgba(245, 166, 35, 0.25);
|
||||
border-color: var(--tf-amber);
|
||||
}
|
||||
|
||||
.tf-wallet-btn.disconnect {
|
||||
border-color: rgba(255, 50, 50, 0.3);
|
||||
color: rgba(255, 50, 50, 0.7);
|
||||
background: rgba(255, 50, 50, 0.08);
|
||||
}
|
||||
|
||||
.tf-wallet-btn.disconnect:hover {
|
||||
border-color: rgba(255, 50, 50, 0.6);
|
||||
background: rgba(255, 50, 50, 0.15);
|
||||
}
|
||||
|
||||
/* ── Form ──────────────────────────────────────────────────── */
|
||||
|
||||
.tf-form {
|
||||
background: rgba(16, 16, 16, 0.85);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--tf-amber-dim);
|
||||
padding: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.tf-form-title {
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--tf-amber);
|
||||
letter-spacing: 3px;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(245, 166, 35, 0.15);
|
||||
}
|
||||
|
||||
.tf-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.tf-form-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.tf-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.tf-field.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.tf-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.6rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.tf-label-hint {
|
||||
font-size: 0.5rem;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.tf-input,
|
||||
.tf-select,
|
||||
.tf-textarea {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(245, 166, 35, 0.15);
|
||||
color: #ffffff;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
outline: none;
|
||||
transition: all 0.25s ease;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.tf-input:focus,
|
||||
.tf-select:focus,
|
||||
.tf-textarea:focus {
|
||||
border-color: var(--tf-amber-dim);
|
||||
box-shadow: 0 0 15px rgba(245, 166, 35, 0.1);
|
||||
}
|
||||
|
||||
.tf-input::placeholder,
|
||||
.tf-textarea::placeholder {
|
||||
color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.tf-select {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23F5A623' viewBox='0 0 16 16'%3E%3Cpath d='M8 12L2 6h12z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.75rem center;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.tf-select option {
|
||||
background: #111;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tf-textarea {
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
/* ── Checkboxes ────────────────────────────────────────────── */
|
||||
|
||||
.tf-checkboxes {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-top: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tf-checkbox-wrap {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tf-checkbox-wrap input[type="checkbox"] {
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid var(--tf-amber-border);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.tf-checkbox-wrap input[type="checkbox"]:checked {
|
||||
background: var(--tf-amber-bg);
|
||||
border-color: var(--tf-amber);
|
||||
}
|
||||
|
||||
.tf-checkbox-wrap input[type="checkbox"]:checked::after {
|
||||
content: '\2713';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--tf-amber);
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.tf-checkbox-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.tf-checkbox-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.6rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.tf-checkbox-rec {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.5rem;
|
||||
color: var(--tf-amber-dim);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Fee Display ───────────────────────────────────────────── */
|
||||
|
||||
.tf-fee-section {
|
||||
grid-column: 1 / -1;
|
||||
background: rgba(245, 166, 35, 0.04);
|
||||
border: 1px solid rgba(245, 166, 35, 0.12);
|
||||
padding: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.tf-fee-title {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.6rem;
|
||||
color: var(--tf-amber);
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tf-fee-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.6rem;
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
|
||||
.tf-fee-label { color: rgba(255, 255, 255, 0.4); }
|
||||
.tf-fee-value { color: rgba(255, 255, 255, 0.7); }
|
||||
|
||||
.tf-fee-total {
|
||||
border-top: 1px solid rgba(245, 166, 35, 0.15);
|
||||
margin-top: 0.35rem;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.tf-fee-total .tf-fee-value {
|
||||
color: var(--tf-amber);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ── Deploy Button ─────────────────────────────────────────── */
|
||||
|
||||
.tf-deploy-section {
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.tf-deploy-btn {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background: rgba(245, 166, 35, 0.1);
|
||||
border: 1px solid var(--tf-amber-dim);
|
||||
color: var(--tf-amber);
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tf-deploy-btn:hover:not(:disabled) {
|
||||
background: rgba(245, 166, 35, 0.2);
|
||||
border-color: var(--tf-amber-bright);
|
||||
box-shadow: 0 0 30px rgba(245, 166, 35, 0.2), inset 0 0 30px rgba(245, 166, 35, 0.05);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.tf-deploy-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.tf-deploy-btn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tf-deploy-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(245, 166, 35, 0.08), transparent);
|
||||
transition: left 0.6s ease;
|
||||
}
|
||||
|
||||
.tf-deploy-btn:hover:not(:disabled)::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
/* ── Status Panel ──────────────────────────────────────────── */
|
||||
|
||||
.tf-status {
|
||||
background: rgba(16, 16, 16, 0.85);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--tf-amber-dim);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tf-status.active { display: block; }
|
||||
|
||||
.tf-status-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tf-status-title {
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.tf-status.building .tf-status-title,
|
||||
.tf-status.awaiting .tf-status-title,
|
||||
.tf-status.confirming .tf-status-title { color: var(--tf-amber); }
|
||||
|
||||
.tf-status.success .tf-status-title { color: var(--tf-green); }
|
||||
.tf-status.success { border-left-color: rgba(20, 241, 149, 0.6); }
|
||||
|
||||
.tf-status.error .tf-status-title { color: var(--tf-red); }
|
||||
.tf-status.error { border-left-color: rgba(255, 68, 68, 0.6); }
|
||||
|
||||
.tf-status-body {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.65rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.tf-status-body a {
|
||||
color: var(--tf-amber);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tf-status-body a:hover { text-decoration: underline; }
|
||||
|
||||
.tf-spinner {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(245, 166, 35, 0.2);
|
||||
border-top-color: var(--tf-amber);
|
||||
border-radius: 50%;
|
||||
animation: tf-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes tf-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ── Result Details ────────────────────────────────────────── */
|
||||
|
||||
.tf-result-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.tf-result-row:last-child { border-bottom: none; }
|
||||
|
||||
.tf-result-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.55rem;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
letter-spacing: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tf-result-value {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.65rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
word-break: break-all;
|
||||
text-align: right;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
/* ── Modals ────────────────────────────────────────────────── */
|
||||
|
||||
.tf-modal {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tf-modal.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tf-modal-backdrop {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.tf-modal-content {
|
||||
position: relative;
|
||||
background: rgba(16, 16, 16, 0.98);
|
||||
border: 1px solid var(--tf-amber-border);
|
||||
border-left: 3px solid var(--tf-amber-dim);
|
||||
max-width: 550px;
|
||||
width: 90%;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.tf-modal-title {
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--tf-amber);
|
||||
letter-spacing: 3px;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(245, 166, 35, 0.15);
|
||||
}
|
||||
|
||||
.tf-modal-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.4rem 0;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.tf-modal-label {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
letter-spacing: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tf-modal-value {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-align: right;
|
||||
max-width: 55%;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.tf-modal-warning {
|
||||
background: rgba(255, 68, 68, 0.05);
|
||||
border: 1px solid rgba(255, 68, 68, 0.15);
|
||||
padding: 0.75rem;
|
||||
margin: 1rem 0;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.55rem;
|
||||
color: rgba(255, 68, 68, 0.75);
|
||||
line-height: 1.7;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.tf-modal-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.tf-modal-cancel {
|
||||
flex: 1;
|
||||
padding: 0.7rem;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.tf-modal-cancel:hover {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.tf-modal-confirm {
|
||||
flex: 1.5;
|
||||
padding: 0.7rem;
|
||||
background: rgba(245, 166, 35, 0.12);
|
||||
border: 1px solid var(--tf-amber-dim);
|
||||
color: var(--tf-amber);
|
||||
font-family: 'Orbitron', monospace;
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.tf-modal-confirm:hover {
|
||||
background: rgba(245, 166, 35, 0.2);
|
||||
border-color: var(--tf-amber);
|
||||
box-shadow: 0 0 20px rgba(245, 166, 35, 0.15);
|
||||
}
|
||||
|
||||
/* ── Wallet Picker ─────────────────────────────────────────── */
|
||||
|
||||
.tf-wallet-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(245, 166, 35, 0.12);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tf-wallet-option:hover {
|
||||
background: rgba(245, 166, 35, 0.08);
|
||||
border-color: var(--tf-amber-border);
|
||||
color: var(--tf-amber);
|
||||
}
|
||||
|
||||
.tf-wallet-install {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.6rem 1rem;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 1px;
|
||||
text-decoration: none;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.tf-wallet-install:hover {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* ── Responsive ────────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.tf-container { padding: 0 1rem 2rem; }
|
||||
.tf-form { padding: 1.25rem; }
|
||||
.tf-modal-content { padding: 1.25rem; }
|
||||
.tf-modal-actions { flex-direction: column; }
|
||||
.tf-result-row { flex-direction: column; gap: 0.25rem; align-items: flex-start; }
|
||||
.tf-result-value { text-align: left; max-width: 100%; }
|
||||
.tf-checkboxes { flex-direction: column; gap: 1rem; }
|
||||
.tf-wallet-bar { flex-direction: column; gap: 0.75rem; text-align: center; }
|
||||
}
|
||||
|
||||
.hidden { display: none !important; }
|
||||
663
js/tokenlauncher.js
Normal file
663
js/tokenlauncher.js
Normal file
|
|
@ -0,0 +1,663 @@
|
|||
/* TOKEN FORGE — SPL Token Launcher
|
||||
Deploys custom SPL tokens on Solana mainnet with Metaplex metadata.
|
||||
Manual instruction encoding for SPL Token + Metaplex programs.
|
||||
Requires @solana/web3.js CDN (solanaWeb3 global).
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ── Constants ──────────────────────────────────────────────
|
||||
const RPC_URL = 'https://api.mainnet-beta.solana.com';
|
||||
const FEE_WALLET = '9NuiHh5wgRPx69BFGP1ZR8kHiBENGoJrXs5GpZzKAyn8';
|
||||
const SERVICE_FEE_LAMPORTS = 100_000_000; // 0.1 SOL
|
||||
const MINT_ACCOUNT_SIZE = 82;
|
||||
|
||||
const TOKEN_PROGRAM = new solanaWeb3.PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
|
||||
const ATA_PROGRAM = new solanaWeb3.PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
|
||||
const METADATA_PROGRAM = new solanaWeb3.PublicKey('metaqbxxUEFHGOqbxLZq71kbiKTfnLRjnNt8GYJoMKM');
|
||||
const SYSTEM_PROGRAM = solanaWeb3.SystemProgram.programId;
|
||||
const RENT_SYSVAR = new solanaWeb3.PublicKey('SysvarRent111111111111111111111111111111111');
|
||||
|
||||
const SOLSCAN_TX = 'https://solscan.io/tx/';
|
||||
const SOLSCAN_TOKEN = 'https://solscan.io/token/';
|
||||
const EXPLORER_TX = 'https://explorer.solana.com/tx/';
|
||||
const MAX_U64 = 18446744073709551615n;
|
||||
|
||||
// ── State ──────────────────────────────────────────────────
|
||||
let walletAddress = null;
|
||||
|
||||
// ── DOM ────────────────────────────────────────────────────
|
||||
const $ = s => document.querySelector(s);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initWallet();
|
||||
initForm();
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// WALLET
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
function initWallet() {
|
||||
window.addEventListener('wallet-connected', e => {
|
||||
walletAddress = e.detail.address;
|
||||
updateWalletUI();
|
||||
});
|
||||
window.addEventListener('wallet-disconnected', () => {
|
||||
walletAddress = null;
|
||||
updateWalletUI();
|
||||
});
|
||||
if (window.solWallet?.connected) walletAddress = window.solWallet.address;
|
||||
updateWalletUI();
|
||||
|
||||
const btn = $('#wallet-btn');
|
||||
if (btn) btn.addEventListener('click', () => {
|
||||
walletAddress ? window.solWallet.disconnect() : showWalletPicker();
|
||||
});
|
||||
}
|
||||
|
||||
function updateWalletUI() {
|
||||
const s = $('#wallet-status'), b = $('#wallet-btn');
|
||||
if (!s || !b) return;
|
||||
if (walletAddress) {
|
||||
s.className = 'tf-wallet-status connected';
|
||||
s.textContent = '\u25CF CONNECTED \u2014 ' + truncAddr(walletAddress);
|
||||
b.className = 'tf-wallet-btn disconnect';
|
||||
b.textContent = 'DISCONNECT';
|
||||
} else {
|
||||
s.className = 'tf-wallet-status';
|
||||
s.textContent = '\u25CB NOT CONNECTED';
|
||||
b.className = 'tf-wallet-btn';
|
||||
b.textContent = 'CONNECT WALLET';
|
||||
}
|
||||
updateDeployBtn();
|
||||
}
|
||||
|
||||
function showWalletPicker() {
|
||||
const wallets = window.solWallet.getAvailableWallets();
|
||||
const known = window.solWallet.KNOWN_WALLETS;
|
||||
let m = $('#tf-wallet-modal');
|
||||
if (!m) {
|
||||
m = document.createElement('div');
|
||||
m.id = 'tf-wallet-modal';
|
||||
m.className = 'tf-modal';
|
||||
document.body.appendChild(m);
|
||||
}
|
||||
|
||||
const names = new Set(wallets.map(w => w.name));
|
||||
const det = wallets.map(w =>
|
||||
'<button class="tf-wallet-option" data-w="' + esc(w.name) + '">' + w.icon + ' ' + esc(w.name) + '</button>'
|
||||
).join('');
|
||||
const inst = known.filter(k => !names.has(k.name)).map(k =>
|
||||
'<a href="' + esc(k.url) + '" target="_blank" rel="noopener" class="tf-wallet-install">' + k.icon + ' ' + esc(k.name) + ' \u2014 INSTALL \u2197</a>'
|
||||
).join('');
|
||||
|
||||
m.innerHTML =
|
||||
'<div class="tf-modal-backdrop"></div>' +
|
||||
'<div class="tf-modal-content">' +
|
||||
'<div class="tf-modal-title">SELECT WALLET</div>' +
|
||||
(det
|
||||
? '<div style="display:flex;flex-direction:column;gap:0.5rem;margin-bottom:1rem">' + det + '</div>'
|
||||
: '<div style="font-family:JetBrains Mono,monospace;font-size:0.6rem;color:rgba(255,255,255,0.3);margin-bottom:1rem">NO WALLETS DETECTED</div>') +
|
||||
(inst
|
||||
? '<div style="font-family:JetBrains Mono,monospace;font-size:0.55rem;color:rgba(255,255,255,0.25);letter-spacing:1px;margin-bottom:0.5rem">INSTALL A WALLET</div><div style="display:flex;flex-direction:column;gap:0.35rem">' + inst + '</div>'
|
||||
: '') +
|
||||
'</div>';
|
||||
|
||||
m.classList.add('active');
|
||||
m.querySelector('.tf-modal-backdrop').onclick = () => m.classList.remove('active');
|
||||
m.querySelectorAll('.tf-wallet-option').forEach(btn => {
|
||||
btn.onclick = async () => {
|
||||
m.classList.remove('active');
|
||||
try { await window.solWallet.connect(btn.dataset.w); } catch (e) {
|
||||
console.warn('[TokenForge] Connect failed:', e.message);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// FORM
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
function initForm() {
|
||||
$('#deploy-btn')?.addEventListener('click', handleDeploy);
|
||||
document.querySelectorAll('.tf-input,.tf-textarea,.tf-select,#revoke-mint,#revoke-freeze').forEach(el => {
|
||||
el.addEventListener('input', updateDeployBtn);
|
||||
el.addEventListener('change', updateDeployBtn);
|
||||
});
|
||||
}
|
||||
|
||||
function formData() {
|
||||
return {
|
||||
name: ($('#token-name')?.value || '').trim(),
|
||||
symbol: ($('#token-symbol')?.value || '').trim().toUpperCase(),
|
||||
supply: ($('#token-supply')?.value || '').trim(),
|
||||
decimals: parseInt($('#token-decimals')?.value || '6', 10),
|
||||
imageUrl: ($('#token-image')?.value || '').trim(),
|
||||
desc: ($('#token-desc')?.value || '').trim(),
|
||||
revokeMint: !!$('#revoke-mint')?.checked,
|
||||
revokeFreeze: !!$('#revoke-freeze')?.checked,
|
||||
};
|
||||
}
|
||||
|
||||
function validate() {
|
||||
const d = formData(), e = [];
|
||||
if (!d.name || d.name.length > 32) e.push('Token name required (max 32 chars)');
|
||||
if (!d.symbol || d.symbol.length > 10) e.push('Symbol required (max 10 chars)');
|
||||
|
||||
// Supply validation
|
||||
const supNum = Number(d.supply);
|
||||
if (!d.supply || !Number.isFinite(supNum) || supNum <= 0 || supNum !== Math.floor(supNum)) {
|
||||
e.push('Supply must be a positive whole number');
|
||||
} else {
|
||||
try {
|
||||
const raw = BigInt(Math.floor(supNum)) * (10n ** BigInt(d.decimals));
|
||||
if (raw > MAX_U64) e.push('Supply \u00D7 10^decimals exceeds u64 max');
|
||||
} catch (_) { e.push('Invalid supply number'); }
|
||||
}
|
||||
if (d.decimals < 0 || d.decimals > 9) e.push('Decimals must be 0-9');
|
||||
return e;
|
||||
}
|
||||
|
||||
function updateDeployBtn() {
|
||||
const b = $('#deploy-btn');
|
||||
if (b) b.disabled = validate().length > 0 || !walletAddress;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// DEPLOY FLOW
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
function handleDeploy() {
|
||||
const errs = validate();
|
||||
if (errs.length) { showStatus('error', 'VALIDATION ERROR', errs.join('<br>')); return; }
|
||||
if (!walletAddress || !window.solWallet?.provider) {
|
||||
showStatus('error', 'WALLET NOT CONNECTED', 'Connect your wallet first.');
|
||||
return;
|
||||
}
|
||||
showConfirmModal(formData());
|
||||
}
|
||||
|
||||
function showConfirmModal(d) {
|
||||
let m = $('#tf-confirm-modal');
|
||||
if (!m) {
|
||||
m = document.createElement('div');
|
||||
m.id = 'tf-confirm-modal';
|
||||
m.className = 'tf-modal';
|
||||
document.body.appendChild(m);
|
||||
}
|
||||
const supFmt = Number(d.supply).toLocaleString();
|
||||
const hasRevoke = d.revokeMint || d.revokeFreeze;
|
||||
|
||||
m.innerHTML =
|
||||
'<div class="tf-modal-backdrop"></div>' +
|
||||
'<div class="tf-modal-content">' +
|
||||
'<div class="tf-modal-title">CONFIRM TOKEN DEPLOYMENT</div>' +
|
||||
mRow('NAME', esc(d.name)) +
|
||||
mRow('SYMBOL', esc(d.symbol)) +
|
||||
mRow('SUPPLY', supFmt) +
|
||||
mRow('DECIMALS', String(d.decimals)) +
|
||||
(d.imageUrl ? mRow('IMAGE', '<span style="font-size:0.5rem">' + esc(d.imageUrl.length > 50 ? d.imageUrl.slice(0, 47) + '...' : d.imageUrl) + '</span>') : '') +
|
||||
(d.revokeMint ? mRow('REVOKE MINT', '<span style="color:#F5A623">YES \u2014 PERMANENT</span>') : '') +
|
||||
(d.revokeFreeze ? mRow('REVOKE FREEZE', '<span style="color:#F5A623">YES \u2014 PERMANENT</span>') : '') +
|
||||
mRow('DEPLOYER', truncAddr(walletAddress)) +
|
||||
'<div style="margin-top:1rem;padding-top:.75rem;border-top:1px solid rgba(245,166,35,.15)">' +
|
||||
mRow('SERVICE FEE', '0.1 SOL') +
|
||||
mRow('NETWORK FEES', '~0.015 SOL') +
|
||||
mRow('ESTIMATED TOTAL', '<span style="color:#F5A623;font-weight:700">~0.115 SOL</span>') +
|
||||
'</div>' +
|
||||
(hasRevoke ? '<div class="tf-modal-warning">\u26A0 AUTHORITY REVOCATION IS PERMANENT AND CANNOT BE UNDONE.' +
|
||||
(d.revokeMint ? ' You will not be able to mint additional tokens.' : '') +
|
||||
(d.revokeFreeze ? ' You will not be able to freeze accounts.' : '') + '</div>' : '') +
|
||||
'<div class="tf-modal-warning">\u26A0 THIS IS A REAL ON-CHAIN TRANSACTION. SOL WILL BE DEDUCTED FROM YOUR WALLET. VERIFY ALL DETAILS BEFORE CONFIRMING.</div>' +
|
||||
'<div class="tf-modal-actions">' +
|
||||
'<button class="tf-modal-cancel" id="confirm-cancel">CANCEL</button>' +
|
||||
'<button class="tf-modal-confirm" id="confirm-deploy">DEPLOY TOKEN</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
m.classList.add('active');
|
||||
m.querySelector('.tf-modal-backdrop').onclick = () => m.classList.remove('active');
|
||||
m.querySelector('#confirm-cancel').onclick = () => m.classList.remove('active');
|
||||
m.querySelector('#confirm-deploy').onclick = () => { m.classList.remove('active'); executeDeploy(d); };
|
||||
}
|
||||
|
||||
async function executeDeploy(data) {
|
||||
try {
|
||||
showStatus('building', 'BUILDING TRANSACTION', 'Generating mint keypair and constructing instructions...');
|
||||
disableForm(true);
|
||||
|
||||
const conn = new solanaWeb3.Connection(RPC_URL, 'confirmed');
|
||||
const payer = new solanaWeb3.PublicKey(walletAddress);
|
||||
const feeWallet = new solanaWeb3.PublicKey(FEE_WALLET);
|
||||
const mintKp = solanaWeb3.Keypair.generate();
|
||||
const mint = mintKp.publicKey;
|
||||
|
||||
// Rent exemption for mint account
|
||||
const mintRent = await conn.getMinimumBalanceForRentExemption(MINT_ACCOUNT_SIZE);
|
||||
|
||||
// Derive Associated Token Account address
|
||||
const [ata] = solanaWeb3.PublicKey.findProgramAddressSync(
|
||||
[payer.toBytes(), TOKEN_PROGRAM.toBytes(), mint.toBytes()],
|
||||
ATA_PROGRAM
|
||||
);
|
||||
|
||||
// Derive Metaplex metadata PDA
|
||||
const [metaPDA] = solanaWeb3.PublicKey.findProgramAddressSync(
|
||||
[new TextEncoder().encode('metadata'), METADATA_PROGRAM.toBytes(), mint.toBytes()],
|
||||
METADATA_PROGRAM
|
||||
);
|
||||
|
||||
// Calculate raw supply with decimals
|
||||
const rawSupply = BigInt(Math.floor(Number(data.supply))) * (10n ** BigInt(data.decimals));
|
||||
|
||||
// ── Build instruction list ──
|
||||
const ixs = [];
|
||||
|
||||
// 1. Create mint account (SystemProgram)
|
||||
ixs.push(solanaWeb3.SystemProgram.createAccount({
|
||||
fromPubkey: payer,
|
||||
newAccountPubkey: mint,
|
||||
space: MINT_ACCOUNT_SIZE,
|
||||
lamports: mintRent,
|
||||
programId: TOKEN_PROGRAM,
|
||||
}));
|
||||
|
||||
// 2. InitializeMint2 — SPL Token instruction 20
|
||||
ixs.push(ixInitMint2(mint, data.decimals, payer, payer));
|
||||
|
||||
// 3. Create Associated Token Account (idempotent)
|
||||
ixs.push(ixCreateATA(payer, ata, payer, mint));
|
||||
|
||||
// 4. MintTo — mint initial supply to user's ATA
|
||||
ixs.push(ixMintTo(mint, ata, payer, rawSupply));
|
||||
|
||||
// 5. Create Metaplex metadata
|
||||
ixs.push(ixCreateMetadata(metaPDA, mint, payer, data));
|
||||
|
||||
// 6. Revoke mint authority (optional)
|
||||
if (data.revokeMint) ixs.push(ixSetAuthority(mint, payer, 0, null));
|
||||
|
||||
// 7. Revoke freeze authority (optional)
|
||||
if (data.revokeFreeze) ixs.push(ixSetAuthority(mint, payer, 1, null));
|
||||
|
||||
// 8. Service fee transfer — 0.1 SOL to site wallet
|
||||
ixs.push(solanaWeb3.SystemProgram.transfer({
|
||||
fromPubkey: payer,
|
||||
toPubkey: feeWallet,
|
||||
lamports: SERVICE_FEE_LAMPORTS,
|
||||
}));
|
||||
|
||||
// ── Build transaction ──
|
||||
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash('confirmed');
|
||||
const tx = new solanaWeb3.Transaction();
|
||||
tx.recentBlockhash = blockhash;
|
||||
tx.feePayer = payer;
|
||||
ixs.forEach(ix => tx.add(ix));
|
||||
|
||||
// Partial sign with the mint keypair (payer signs via wallet)
|
||||
tx.partialSign(mintKp);
|
||||
|
||||
// ── Sign & send ──
|
||||
showStatus('awaiting', 'AWAITING WALLET SIGNATURE', 'Please approve the transaction in your wallet...');
|
||||
|
||||
let sig;
|
||||
if (window.solWallet.isWalletStandard) {
|
||||
sig = await wsSend(tx, conn);
|
||||
} else {
|
||||
sig = await legacySend(tx, conn);
|
||||
}
|
||||
|
||||
// ── Confirm on-chain ──
|
||||
showStatus('confirming', 'CONFIRMING ON-CHAIN',
|
||||
'Transaction submitted \u2014 awaiting confirmation...<br>' +
|
||||
rRow('TX', '<a href="' + SOLSCAN_TX + sig + '" target="_blank" rel="noopener">' + truncAddr(sig) + ' \u2197</a>')
|
||||
);
|
||||
|
||||
const conf = await conn.confirmTransaction(
|
||||
{ signature: sig, blockhash, lastValidBlockHeight },
|
||||
'confirmed'
|
||||
);
|
||||
if (conf.value.err) throw new Error('On-chain failure: ' + JSON.stringify(conf.value.err));
|
||||
|
||||
// ── Success ──
|
||||
const mintAddr = mint.toBase58();
|
||||
showStatus('success', data.symbol + ' DEPLOYED SUCCESSFULLY',
|
||||
rRow('TOKEN', esc(data.name) + ' (' + esc(data.symbol) + ')') +
|
||||
rRow('SUPPLY', Number(data.supply).toLocaleString()) +
|
||||
rRow('DECIMALS', String(data.decimals)) +
|
||||
rRow('MINT ADDRESS', '<span style="font-size:0.55rem">' + mintAddr + '</span>') +
|
||||
rRow('SOLSCAN', '<a href="' + SOLSCAN_TOKEN + mintAddr + '" target="_blank" rel="noopener">View Token \u2197</a>') +
|
||||
rRow('EXPLORER', '<a href="' + EXPLORER_TX + sig + '" target="_blank" rel="noopener">View Transaction \u2197</a>') +
|
||||
rRow('TX SIGNATURE', '<a href="' + SOLSCAN_TX + sig + '" target="_blank" rel="noopener" style="font-size:0.5rem">' + truncAddr(sig) + ' \u2197</a>')
|
||||
);
|
||||
|
||||
} catch (err) {
|
||||
console.error('[TokenForge] Deploy error:', err);
|
||||
let msg = err.message || 'Unknown error';
|
||||
if (/reject|denied|cancel|declined|disapproved/i.test(msg))
|
||||
msg = 'Transaction was rejected by your wallet.';
|
||||
else if (/insufficient|not enough|0x1/i.test(msg))
|
||||
msg = 'Insufficient SOL balance. You need approximately 0.115 SOL.';
|
||||
else if (/blockhash|expired|block height/i.test(msg))
|
||||
msg = 'Transaction expired. Please try again.';
|
||||
else if (/network|fetch|failed to fetch/i.test(msg))
|
||||
msg = 'Network error. Check your connection and try again.';
|
||||
showStatus('error', 'DEPLOYMENT FAILED', esc(msg));
|
||||
} finally {
|
||||
disableForm(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// SIGN & SEND HELPERS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
async function legacySend(tx, conn) {
|
||||
const p = window.solWallet.provider;
|
||||
|
||||
// Try signAndSendTransaction first (Phantom, Solflare, etc.)
|
||||
if (typeof p.signAndSendTransaction === 'function') {
|
||||
try {
|
||||
const r = await p.signAndSendTransaction(tx, {
|
||||
skipPreflight: false,
|
||||
preflightCommitment: 'confirmed',
|
||||
});
|
||||
return r.signature || r;
|
||||
} catch (e) {
|
||||
if (/reject|denied|cancel|declined|disapproved/i.test(e.message || '')) throw e;
|
||||
console.warn('[TokenForge] signAndSendTransaction failed, fallback:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: signTransaction + manual send
|
||||
if (typeof p.signTransaction !== 'function')
|
||||
throw new Error('Wallet does not support transaction signing.');
|
||||
const signed = await p.signTransaction(tx);
|
||||
return await conn.sendRawTransaction(signed.serialize(), {
|
||||
skipPreflight: false,
|
||||
preflightCommitment: 'confirmed',
|
||||
});
|
||||
}
|
||||
|
||||
async function wsSend(tx, conn) {
|
||||
const w = window.solWallet.provider;
|
||||
const acct = w.accounts?.[0];
|
||||
if (!acct) throw new Error('No wallet account available.');
|
||||
|
||||
const bytes = tx.serialize({ requireAllSignatures: false, verifySignatures: false });
|
||||
|
||||
// Try signAndSendTransaction feature
|
||||
const feat = w.features?.['solana:signAndSendTransaction'];
|
||||
if (feat) {
|
||||
try {
|
||||
const res = await feat.signAndSendTransaction({
|
||||
account: acct,
|
||||
transaction: bytes,
|
||||
chain: 'solana:mainnet',
|
||||
});
|
||||
const r = Array.isArray(res) ? res[0] : res;
|
||||
if (r.signature) {
|
||||
return typeof r.signature === 'string' ? r.signature : b58(new Uint8Array(r.signature));
|
||||
}
|
||||
return r;
|
||||
} catch (e) {
|
||||
if (/reject|denied|cancel|declined|disapproved/i.test(e.message || '')) throw e;
|
||||
console.warn('[TokenForge] WS signAndSend failed, fallback:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: signTransaction + manual send
|
||||
const sf = w.features?.['solana:signTransaction'];
|
||||
if (!sf) throw new Error('Wallet does not support Solana signing.');
|
||||
const res = await sf.signTransaction({
|
||||
account: acct,
|
||||
transaction: bytes,
|
||||
chain: 'solana:mainnet',
|
||||
});
|
||||
const r = Array.isArray(res) ? res[0] : res;
|
||||
return await conn.sendRawTransaction(r.signedTransaction, {
|
||||
skipPreflight: false,
|
||||
preflightCommitment: 'confirmed',
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// SPL TOKEN INSTRUCTIONS (manual binary encoding)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* InitializeMint2 — SPL Token instruction index 20
|
||||
* Data: [20, decimals(u8), mintAuthority(32), freezeOption(u8), freezeAuthority(32)]
|
||||
* Accounts: [mint(writable)]
|
||||
*/
|
||||
function ixInitMint2(mint, decimals, mintAuth, freezeAuth) {
|
||||
const d = new Uint8Array(67);
|
||||
d[0] = 20;
|
||||
d[1] = decimals;
|
||||
d.set(mintAuth.toBytes(), 2);
|
||||
d[34] = 1; // COption::Some
|
||||
d.set(freezeAuth.toBytes(), 35);
|
||||
return new solanaWeb3.TransactionInstruction({
|
||||
programId: TOKEN_PROGRAM,
|
||||
keys: [{ pubkey: mint, isSigner: false, isWritable: true }],
|
||||
data: d,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* MintTo — SPL Token instruction index 7
|
||||
* Data: [7, amount(u64 LE)]
|
||||
* Accounts: [mint(writable), destination(writable), authority(signer)]
|
||||
*/
|
||||
function ixMintTo(mint, dest, auth, amount) {
|
||||
const d = new Uint8Array(9);
|
||||
d[0] = 7;
|
||||
writeU64(d, amount, 1);
|
||||
return new solanaWeb3.TransactionInstruction({
|
||||
programId: TOKEN_PROGRAM,
|
||||
keys: [
|
||||
{ pubkey: mint, isSigner: false, isWritable: true },
|
||||
{ pubkey: dest, isSigner: false, isWritable: true },
|
||||
{ pubkey: auth, isSigner: true, isWritable: false },
|
||||
],
|
||||
data: d,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SetAuthority — SPL Token instruction index 6
|
||||
* Data: [6, authorityType(u8), newAuthorityOption(u8), newAuthority?(32)]
|
||||
* authorityType: 0=MintTokens, 1=FreezeAccount
|
||||
* Accounts: [account(writable), currentAuthority(signer)]
|
||||
*/
|
||||
function ixSetAuthority(acct, curAuth, authType, newAuth) {
|
||||
const hasNew = newAuth !== null;
|
||||
const d = new Uint8Array(hasNew ? 35 : 3);
|
||||
d[0] = 6;
|
||||
d[1] = authType;
|
||||
d[2] = hasNew ? 1 : 0;
|
||||
if (hasNew) d.set(newAuth.toBytes(), 3);
|
||||
return new solanaWeb3.TransactionInstruction({
|
||||
programId: TOKEN_PROGRAM,
|
||||
keys: [
|
||||
{ pubkey: acct, isSigner: false, isWritable: true },
|
||||
{ pubkey: curAuth, isSigner: true, isWritable: false },
|
||||
],
|
||||
data: d,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateIdempotent — Associated Token Account Program instruction 1
|
||||
* Data: [1]
|
||||
* Accounts: [payer(s,w), ata(w), owner, mint, systemProgram, tokenProgram]
|
||||
*/
|
||||
function ixCreateATA(payer, ata, owner, mint) {
|
||||
return new solanaWeb3.TransactionInstruction({
|
||||
programId: ATA_PROGRAM,
|
||||
keys: [
|
||||
{ pubkey: payer, isSigner: true, isWritable: true },
|
||||
{ pubkey: ata, isSigner: false, isWritable: true },
|
||||
{ pubkey: owner, isSigner: false, isWritable: false },
|
||||
{ pubkey: mint, isSigner: false, isWritable: false },
|
||||
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
||||
{ pubkey: TOKEN_PROGRAM, isSigner: false, isWritable: false },
|
||||
],
|
||||
data: new Uint8Array([1]),
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// METAPLEX TOKEN METADATA INSTRUCTION
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* CreateMetadataAccountV3 — Metaplex instruction index 33
|
||||
* Borsh-serialized: [33, DataV2(...), isMutable(bool), collectionDetails(Option)]
|
||||
*/
|
||||
function ixCreateMetadata(metaPDA, mint, authority, tokenData) {
|
||||
const enc = new TextEncoder();
|
||||
const nameB = enc.encode(tokenData.name);
|
||||
const symB = enc.encode(tokenData.symbol);
|
||||
const uriB = enc.encode(tokenData.imageUrl || '');
|
||||
|
||||
// Layout: 1(ix) + string(name) + string(symbol) + string(uri) + u16 + opt + opt + opt + bool + opt
|
||||
// Borsh string = 4-byte u32 LE length prefix + UTF-8 bytes
|
||||
const sz = 1
|
||||
+ (4 + nameB.length)
|
||||
+ (4 + symB.length)
|
||||
+ (4 + uriB.length)
|
||||
+ 2 // seller_fee_basis_points
|
||||
+ 1 // creators: None
|
||||
+ 1 // collection: None
|
||||
+ 1 // uses: None
|
||||
+ 1 // is_mutable
|
||||
+ 1; // collection_details: None
|
||||
|
||||
const d = new Uint8Array(sz);
|
||||
let o = 0;
|
||||
|
||||
d[o++] = 33; // instruction index
|
||||
|
||||
// DataV2.name
|
||||
writeU32(d, nameB.length, o); o += 4;
|
||||
d.set(nameB, o); o += nameB.length;
|
||||
|
||||
// DataV2.symbol
|
||||
writeU32(d, symB.length, o); o += 4;
|
||||
d.set(symB, o); o += symB.length;
|
||||
|
||||
// DataV2.uri
|
||||
writeU32(d, uriB.length, o); o += 4;
|
||||
d.set(uriB, o); o += uriB.length;
|
||||
|
||||
// DataV2.seller_fee_basis_points (u16 LE) — 0 for fungible tokens
|
||||
d[o++] = 0; d[o++] = 0;
|
||||
|
||||
// DataV2.creators: None
|
||||
d[o++] = 0;
|
||||
// DataV2.collection: None
|
||||
d[o++] = 0;
|
||||
// DataV2.uses: None
|
||||
d[o++] = 0;
|
||||
|
||||
// is_mutable: true
|
||||
d[o++] = 1;
|
||||
|
||||
// collection_details: None
|
||||
d[o++] = 0;
|
||||
|
||||
return new solanaWeb3.TransactionInstruction({
|
||||
programId: METADATA_PROGRAM,
|
||||
keys: [
|
||||
{ pubkey: metaPDA, isSigner: false, isWritable: true }, // metadata PDA
|
||||
{ pubkey: mint, isSigner: false, isWritable: false }, // mint
|
||||
{ pubkey: authority, isSigner: true, isWritable: false }, // mint authority
|
||||
{ pubkey: authority, isSigner: true, isWritable: true }, // payer
|
||||
{ pubkey: authority, isSigner: false, isWritable: false }, // update authority
|
||||
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false }, // system program
|
||||
{ pubkey: RENT_SYSVAR, isSigner: false, isWritable: false }, // rent (optional but safe)
|
||||
],
|
||||
data: d,
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// BYTE HELPERS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
function writeU32(buf, val, off) {
|
||||
buf[off] = val & 0xFF;
|
||||
buf[off + 1] = (val >> 8) & 0xFF;
|
||||
buf[off + 2] = (val >> 16) & 0xFF;
|
||||
buf[off + 3] = (val >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
function writeU64(buf, val, off) {
|
||||
let v = BigInt(val);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
buf[off + i] = Number(v & 0xFFn);
|
||||
v >>= 8n;
|
||||
}
|
||||
}
|
||||
|
||||
/** Base58 encode a Uint8Array (for WalletStandard signature conversion) */
|
||||
function b58(bytes) {
|
||||
const A = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
let r = '', n = 0n;
|
||||
for (const b of bytes) n = n * 256n + BigInt(b);
|
||||
while (n > 0n) { r = A[Number(n % 58n)] + r; n /= 58n; }
|
||||
for (const b of bytes) { if (b === 0) r = '1' + r; else break; }
|
||||
return r || '1';
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// UI HELPERS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
function showStatus(state, title, body) {
|
||||
const panel = $('#status-panel');
|
||||
if (!panel) return;
|
||||
panel.className = 'tf-status active ' + state;
|
||||
|
||||
const sp = $('#status-spinner');
|
||||
if (sp) sp.style.display = (state === 'success' || state === 'error') ? 'none' : 'inline-block';
|
||||
|
||||
const t = $('#status-title');
|
||||
if (t) t.textContent = title;
|
||||
|
||||
const b = $('#status-body');
|
||||
if (b) b.innerHTML = body;
|
||||
|
||||
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
function disableForm(yes) {
|
||||
document.querySelectorAll('.tf-input,.tf-textarea,.tf-select,#revoke-mint,#revoke-freeze').forEach(el => el.disabled = yes);
|
||||
const btn = $('#deploy-btn');
|
||||
if (btn) btn.disabled = yes;
|
||||
}
|
||||
|
||||
/** Result row for status body */
|
||||
function rRow(label, value) {
|
||||
return '<div class="tf-result-row"><span class="tf-result-label">' + label + '</span><span class="tf-result-value">' + value + '</span></div>';
|
||||
}
|
||||
|
||||
/** Modal confirm row */
|
||||
function mRow(label, value) {
|
||||
return '<div class="tf-modal-row"><span class="tf-modal-label">' + label + '</span><span class="tf-modal-value">' + value + '</span></div>';
|
||||
}
|
||||
|
||||
function truncAddr(a) {
|
||||
if (!a || a.length < 12) return a || '';
|
||||
return a.slice(0, 4) + '...' + a.slice(-4);
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
})();
|
||||
186
tokenlauncher/index.html
Normal file
186
tokenlauncher/index.html
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>JAESWIFT // TOKEN FORGE</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/section.css">
|
||||
<link rel="stylesheet" href="/css/tokenlauncher.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="scanline-overlay"></div>
|
||||
<div class="grid-bg"></div>
|
||||
|
||||
<nav class="nav-main" id="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="/" class="nav-logo">
|
||||
<span class="logo-bracket">[</span> JAE <span class="logo-bracket">]</span>
|
||||
</a>
|
||||
<button class="nav-toggle" id="navToggle" aria-label="Menu">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
<ul class="nav-menu" id="navMenu"></ul>
|
||||
<div class="nav-status">
|
||||
<span class="nav-clock" id="navClock"></span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a href="/">HOME</a>
|
||||
<span class="separator">/</span>
|
||||
<a href="/armoury">ARMOURY</a>
|
||||
<span class="separator">/</span>
|
||||
<a href="/armoury/lab.html">LAB</a>
|
||||
<span class="separator">/</span>
|
||||
<span class="current">TOKEN FORGE</span>
|
||||
</div>
|
||||
|
||||
<section class="section-header" style="padding-top: calc(var(--nav-height) + 1.5rem);">
|
||||
<div class="section-header-label">LAB // SPL TOKEN LAUNCHER</div>
|
||||
<h1 class="section-header-title">TOKEN FORGE</h1>
|
||||
<p class="section-header-sub">> Deploy custom SPL tokens on Solana. Set name, symbol, supply, and metadata — launch in one transaction.</p>
|
||||
</section>
|
||||
|
||||
<div class="tf-container">
|
||||
|
||||
<!-- Wallet Bar -->
|
||||
<div class="tf-wallet-bar">
|
||||
<div class="tf-wallet-status" id="wallet-status">○ NOT CONNECTED</div>
|
||||
<button class="tf-wallet-btn" id="wallet-btn">CONNECT WALLET</button>
|
||||
</div>
|
||||
|
||||
<!-- Status Panel (hidden by default, shown during deploy) -->
|
||||
<div class="tf-status" id="status-panel">
|
||||
<div class="tf-status-header">
|
||||
<div class="tf-spinner" id="status-spinner"></div>
|
||||
<div class="tf-status-title" id="status-title">BUILDING TRANSACTION</div>
|
||||
</div>
|
||||
<div class="tf-status-body" id="status-body"></div>
|
||||
</div>
|
||||
|
||||
<!-- Token Configuration Form -->
|
||||
<div class="tf-form">
|
||||
<div class="tf-form-title">TOKEN CONFIGURATION</div>
|
||||
<div class="tf-form-grid">
|
||||
|
||||
<!-- Token Name -->
|
||||
<div class="tf-field">
|
||||
<label class="tf-label" for="token-name">TOKEN NAME</label>
|
||||
<input type="text" class="tf-input" id="token-name" placeholder="e.g. JAESWIFT TOKEN" maxlength="32" autocomplete="off" spellcheck="false">
|
||||
<div class="tf-label-hint">Max 32 characters. Displayed on explorers and wallets.</div>
|
||||
</div>
|
||||
|
||||
<!-- Token Symbol -->
|
||||
<div class="tf-field">
|
||||
<label class="tf-label" for="token-symbol">SYMBOL / TICKER</label>
|
||||
<input type="text" class="tf-input" id="token-symbol" placeholder="e.g. JAE" maxlength="10" autocomplete="off" spellcheck="false" style="text-transform:uppercase">
|
||||
<div class="tf-label-hint">Max 10 characters. The trading ticker for your token.</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Supply -->
|
||||
<div class="tf-field">
|
||||
<label class="tf-label" for="token-supply">TOTAL SUPPLY</label>
|
||||
<input type="number" class="tf-input" id="token-supply" placeholder="e.g. 1000000000" min="1" step="1" autocomplete="off">
|
||||
<div class="tf-label-hint">Whole number. The total number of tokens to mint.</div>
|
||||
</div>
|
||||
|
||||
<!-- Decimals -->
|
||||
<div class="tf-field">
|
||||
<label class="tf-label" for="token-decimals">DECIMALS</label>
|
||||
<select class="tf-select" id="token-decimals">
|
||||
<option value="0">0 — No fractions</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
<option value="6" selected>6 — Standard (like USDC)</option>
|
||||
<option value="7">7</option>
|
||||
<option value="8">8</option>
|
||||
<option value="9">9 — Maximum</option>
|
||||
</select>
|
||||
<div class="tf-label-hint">6 is standard for most tokens. Determines fractional precision.</div>
|
||||
</div>
|
||||
|
||||
<!-- Token Image URL -->
|
||||
<div class="tf-field full-width">
|
||||
<label class="tf-label" for="token-image">TOKEN IMAGE URL</label>
|
||||
<input type="url" class="tf-input" id="token-image" placeholder="https://example.com/token-logo.png" autocomplete="off" spellcheck="false">
|
||||
<div class="tf-label-hint">Optional. URL to your token’s logo image (PNG/SVG). Must be publicly accessible and hosted elsewhere.</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="tf-field full-width">
|
||||
<label class="tf-label" for="token-desc">DESCRIPTION</label>
|
||||
<textarea class="tf-textarea" id="token-desc" placeholder="A brief description of your token..." rows="3" spellcheck="false"></textarea>
|
||||
<div class="tf-label-hint">Optional. Shown on token explorers and metadata records.</div>
|
||||
</div>
|
||||
|
||||
<!-- Authority Revocation -->
|
||||
<div class="tf-checkboxes">
|
||||
<label class="tf-checkbox-wrap">
|
||||
<input type="checkbox" id="revoke-mint" checked>
|
||||
<div class="tf-checkbox-text">
|
||||
<span class="tf-checkbox-label">REVOKE MINT AUTHORITY</span>
|
||||
<span class="tf-checkbox-rec">RECOMMENDED — Prevents creating more tokens after launch</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="tf-checkbox-wrap">
|
||||
<input type="checkbox" id="revoke-freeze" checked>
|
||||
<div class="tf-checkbox-text">
|
||||
<span class="tf-checkbox-label">REVOKE FREEZE AUTHORITY</span>
|
||||
<span class="tf-checkbox-rec">RECOMMENDED — Prevents freezing holder wallets</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Fee Breakdown -->
|
||||
<div class="tf-fee-section">
|
||||
<div class="tf-fee-title">COST BREAKDOWN</div>
|
||||
<div class="tf-fee-row">
|
||||
<span class="tf-fee-label">SERVICE FEE</span>
|
||||
<span class="tf-fee-value">0.1 SOL</span>
|
||||
</div>
|
||||
<div class="tf-fee-row">
|
||||
<span class="tf-fee-label">NETWORK FEES (rent + tx)</span>
|
||||
<span class="tf-fee-value">~0.015 SOL</span>
|
||||
</div>
|
||||
<div class="tf-fee-row tf-fee-total">
|
||||
<span class="tf-fee-label">ESTIMATED TOTAL</span>
|
||||
<span class="tf-fee-value">~0.115 SOL</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deploy Button -->
|
||||
<div class="tf-deploy-section">
|
||||
<button class="tf-deploy-btn" id="deploy-btn" disabled>DEPLOY TOKEN</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="footer-container">
|
||||
<div class="footer-left">
|
||||
<span class="footer-logo">[JAE]</span>
|
||||
<span class="footer-copy">© 2026 JAESWIFT.XYZ</span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span class="footer-signal">SIGNAL ████<span class="signal-flicker">█</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/js/wallet-connect.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script src="/js/clock.js"></script>
|
||||
<script src="https://unpkg.com/@solana/web3.js@1.98.0/lib/index.iife.min.js"></script>
|
||||
<script src="/js/tokenlauncher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Reference in a new issue