Compare commits
4
Commits
mtm-deploy
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f196bf759 | ||
|
|
41add0a99a | ||
|
|
ed08ace0a4 | ||
|
|
ac5dfe6966 |
+3
-3
@@ -23,10 +23,10 @@ NEXT_PUBLIC_REGISTRY_GQL_ENDPOINT=https://laconicd-mainnet-1.laconic.com/graphql
|
||||
NEXT_PUBLIC_ALNT_COST_LRN=lrn://laconic/pricing/alnt
|
||||
NEXT_PUBLIC_DEPLOYMENT_COST_LRN=lrn://laconic/pricing/webapp-deployment
|
||||
REGISTRY_GAS_PRICE=0.001
|
||||
REGISTRY_BOND_ID=5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2
|
||||
REGISTRY_AUTHORITY=laconic-deploy
|
||||
REGISTRY_BOND_ID=5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2
|
||||
REGISTRY_USER_KEY=
|
||||
|
||||
# Application Configuration
|
||||
DEPLOYER_LRN=
|
||||
NEXT_PUBLIC_DOMAIN_SUFFIX=
|
||||
DEPLOYER_LRN=lrn://vaasl-provider/deployers/webapp-deployer-api.apps.vaasl.io
|
||||
NEXT_PUBLIC_DOMAIN_SUFFIX=apps.vaasl.io
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
- Configure `userKey` and `bondId` in the [registry CLI config](./config.yml):
|
||||
|
||||
- User key should be of the account that owns the `laconic-deploy` authority (owner account address: `laconic1kwx2jm6vscz38qlyujvq6msujmk8l3zangqahs`)
|
||||
- The bond should also be owned by same user (owned bond's ID: `230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38`)
|
||||
- The bond should also be owned by same user (owned bond's ID: `5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2`)
|
||||
- If the authority is not available, follow [these steps to reserve a new authority](./publish-pricing.md#reserve-a-new-authority-optional)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: 0.0.1
|
||||
repository_ref: f6c6147340a8e173520af9b27ffcbc9d5e076fed
|
||||
version: 0.0.4
|
||||
repository_ref: bc0d10d4c38d2d20de88ac8352ab8571bbbffef4
|
||||
repository: ["https://git.vdb.to/LaconicNetwork/gor-deploy"]
|
||||
app_type: webapp
|
||||
name: gor-deploy
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@
|
||||
"@solana/web3.js": "^1.98.2",
|
||||
"axios": "^1.6.8",
|
||||
"bn.js": "^5.2.2",
|
||||
"next": "15.3.1",
|
||||
"next": "15.3.8",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
@@ -36,7 +36,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-next": "15.3.1",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import axios from 'axios';
|
||||
import assert from 'assert';
|
||||
|
||||
import { Connection } from '@solana/web3.js';
|
||||
import { Connection, ParsedTransactionWithMeta } from '@solana/web3.js';
|
||||
|
||||
import { verifyUnusedSolanaPayment } from '@/utils/solana-verify';
|
||||
import { transferLNTTokens } from '@/services/laconic-transfer';
|
||||
@@ -25,6 +25,7 @@ const ALLOWED_SLIPPAGE_FACTOR = 0.2
|
||||
|
||||
// Use CAIP convention for chain ID: namespace + reference
|
||||
const SOLANA_CHAIN_ID = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; // Solana mainnet
|
||||
const GORBAGANA_CHAIN_ID = 'gorbagana:533uBE9RRquhTBqEX58oV52FdTTsReMd' // Gorbagana chain (first 32 characters of gorbagana genesis hash. Following solana CAIP chain ID pattern)
|
||||
|
||||
// Sleep helper function
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
@@ -167,6 +168,7 @@ export async function POST(request: NextRequest) {
|
||||
// First check if the request body is valid JSON
|
||||
let url, txHash, senderPublicKey, paymentMethod;
|
||||
let connection: Connection;
|
||||
let parsedTx: ParsedTransactionWithMeta | null;
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
@@ -186,6 +188,8 @@ export async function POST(request: NextRequest) {
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
parsedTx = tx;
|
||||
|
||||
const signerKeys = tx.transaction.message.accountKeys
|
||||
.filter(k => k.signer)
|
||||
.map(k => k.pubkey.toBase58());
|
||||
@@ -243,9 +247,13 @@ export async function POST(request: NextRequest) {
|
||||
const requiredAmountInBaseUnits = requiredTokenInfo.requiredAmountInBaseUnits;
|
||||
const expectedTokenAmount = Math.round(requiredAmountInBaseUnits - ALLOWED_SLIPPAGE_FACTOR * requiredAmountInBaseUnits);
|
||||
|
||||
if (!parsedTx) {
|
||||
throw new Error(`Unable to find the tx with hash: ${txHash}`)
|
||||
}
|
||||
|
||||
const solanaPaymentResult = await verifyUnusedSolanaPayment(
|
||||
connection,
|
||||
txHash,
|
||||
parsedTx,
|
||||
new BN(expectedTokenAmount),
|
||||
paymentMethod,
|
||||
);
|
||||
@@ -457,7 +465,7 @@ export async function POST(request: NextRequest) {
|
||||
repository_ref: fullHash,
|
||||
},
|
||||
external_payment: {
|
||||
chain_id: SOLANA_CHAIN_ID,
|
||||
chain_id: paymentMethod === PaymentMethod.SPL_TOKEN ? SOLANA_CHAIN_ID : GORBAGANA_CHAIN_ID,
|
||||
tx_hash: txHash,
|
||||
pubkey: senderPublicKey
|
||||
},
|
||||
@@ -516,4 +524,4 @@ export async function POST(request: NextRequest) {
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-20
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||
import { useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
|
||||
@@ -27,6 +27,14 @@ const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: f
|
||||
// RPC endpoint reference: https://docs.gorbagana.wtf/testnet-v2-devnet.html
|
||||
const GORBAGANA_GENESIS_HASH = '533uBE9RRquhTBqEX58oV52FdTTsReMdAvaUvP6hNjsn';
|
||||
|
||||
// Use following curl request to get Solana chain genesis hash:
|
||||
// curl https://api.mainnet-beta.solana.com \
|
||||
// -X POST \
|
||||
// -H "Content-Type: application/json" \
|
||||
// --data '{"jsonrpc":"2.0","id":1,"method":"getGenesisHash"}'
|
||||
// RPC endpoint reference: https://solana.com/docs/references/clusters#on-a-high-level
|
||||
const SOLANA_GENESIS_HASH = '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d';
|
||||
|
||||
export default function Home() {
|
||||
const { wallet, connected, publicKey, disconnect } = useWallet();
|
||||
|
||||
@@ -42,7 +50,8 @@ export default function Home() {
|
||||
const [appName, setAppName] = useState<string | null>(null);
|
||||
const [repoUrl, setRepoUrl] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [incorrectChainWarining, setIncorrectChainWarining] = useState<string | null>(null);
|
||||
const [incorrectChainWarning, setIncorrectChainWarning] = useState<string | null>(null);
|
||||
const [isFetchingChainGenesisHash, setIsFetchingChainGenesisHash] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_NAT_GOR_TRANSFER_ENABLED) {
|
||||
@@ -51,20 +60,38 @@ export default function Home() {
|
||||
}, [setSelectedPaymentMethod]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wallet || wallet.adapter.name !== BackpackWalletName || selectedPaymentMethod !== PaymentMethod.NAT_GOR) {
|
||||
setIncorrectChainWarning(null);
|
||||
if (!wallet || wallet.adapter.name !== BackpackWalletName || !connected || !selectedPaymentMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const warnOnIncorrectChain = async () => {
|
||||
// @ts-expect-error: backpack exists on window object
|
||||
const genesisHash = await window.backpack.solana.connection.getGenesisHash();
|
||||
if (genesisHash !== GORBAGANA_GENESIS_HASH) {
|
||||
setIncorrectChainWarining("WARNING: Unsupported chain selected in wallet. Please switch to Gorbagana chain")
|
||||
setIsFetchingChainGenesisHash(true);
|
||||
|
||||
try {
|
||||
// @ts-expect-error: backpack exists on window object
|
||||
const genesisHash = await window.backpack.solana.connection.getGenesisHash();
|
||||
|
||||
const expectedGenesisHash = selectedPaymentMethod === PaymentMethod.NAT_GOR
|
||||
? GORBAGANA_GENESIS_HASH
|
||||
: SOLANA_GENESIS_HASH;
|
||||
|
||||
const expectedChainName = selectedPaymentMethod === PaymentMethod.NAT_GOR
|
||||
? "Gorbagana"
|
||||
: "Solana";
|
||||
|
||||
if (genesisHash !== expectedGenesisHash) {
|
||||
setIncorrectChainWarning(
|
||||
`Unsupported network selected in wallet. Please switch to network for ${expectedChainName} chain and reconnect the wallet.`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsFetchingChainGenesisHash(false);
|
||||
}
|
||||
}
|
||||
|
||||
warnOnIncorrectChain();
|
||||
}, [wallet, selectedPaymentMethod]);
|
||||
}, [wallet, selectedPaymentMethod, connected]);
|
||||
|
||||
// Track previous payment method to detect switches
|
||||
const previousPaymentMethodRef = useRef<PaymentMethod | null>(null);
|
||||
@@ -90,7 +117,7 @@ export default function Home() {
|
||||
};
|
||||
|
||||
// Helper function to check if current wallet is compatible with selected payment method
|
||||
const isWalletCompatible = () => {
|
||||
const isWalletCompatible = useMemo(() => {
|
||||
if (!selectedPaymentMethod || !wallet) return false;
|
||||
|
||||
const walletName = wallet.adapter.name.toLowerCase();
|
||||
@@ -99,9 +126,9 @@ export default function Home() {
|
||||
if (selectedPaymentMethod === PaymentMethod.NAT_GOR) {
|
||||
return isBackpack; // Only Backpack for native GOR
|
||||
} else {
|
||||
return !isBackpack; // Only non-Backpack wallets for SPL tokens
|
||||
return true; // Only non-Backpack wallets for SPL tokens
|
||||
}
|
||||
};
|
||||
}, [selectedPaymentMethod, wallet]);
|
||||
|
||||
const handlePaymentComplete = useCallback(async (hash: string, paymentMethod: PaymentMethod) => {
|
||||
if (!publicKey || !url) {
|
||||
@@ -193,7 +220,7 @@ export default function Home() {
|
||||
<div className="text-left">
|
||||
<h3 className="font-semibold text-lg mb-2">{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} Token</h3>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--muted-foreground)' }}>
|
||||
Compatible with: Phantom, Solflare
|
||||
Compatible with: All Solana compatible wallets
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
@@ -225,23 +252,28 @@ export default function Home() {
|
||||
<div className="flex flex-col items-center space-y-3">
|
||||
<div className="flex items-center">
|
||||
<span className="w-3 h-3 rounded-full mr-2" style={{
|
||||
backgroundColor: isWalletCompatible() ? 'var(--success)' : 'var(--destructive)'
|
||||
backgroundColor: isWalletCompatible ? 'var(--success)' : 'var(--destructive)'
|
||||
}}></span>
|
||||
<p className="font-medium" style={{
|
||||
color: isWalletCompatible() ? 'var(--success)' : 'var(--destructive)'
|
||||
color: isWalletCompatible ? 'var(--success)' : 'var(--destructive)'
|
||||
}}>
|
||||
{isWalletCompatible() ? 'Compatible' : 'Incompatible'} Wallet ({wallet?.adapter.name})
|
||||
{isWalletCompatible ? 'Compatible' : 'Incompatible'} Wallet ({wallet?.adapter.name})
|
||||
</p>
|
||||
</div>
|
||||
{!isWalletCompatible() && (
|
||||
{!isWalletCompatible && (
|
||||
<p className="text-sm text-amber-400">
|
||||
This wallet is not compatible with {PAYMENT_METHOD_LABELS[selectedPaymentMethod]} payments.
|
||||
Please select a different wallet or payment method.
|
||||
</p>
|
||||
)}
|
||||
{incorrectChainWarining && (
|
||||
{ isFetchingChainGenesisHash && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Checking wallet network compatibility...
|
||||
</p>
|
||||
)}
|
||||
{incorrectChainWarning && (
|
||||
<p className="text-sm text-amber-400">
|
||||
{incorrectChainWarining}
|
||||
{incorrectChainWarning}
|
||||
</p>
|
||||
)}
|
||||
<div className="w-full p-3 rounded-md" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
|
||||
@@ -276,18 +308,19 @@ export default function Home() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step 3: URL Input */}
|
||||
<div className="mb-8 p-6 rounded-lg" style={{
|
||||
background: 'var(--muted-light)',
|
||||
borderLeft: '4px solid var(--primary)',
|
||||
opacity: (connected && isWalletCompatible()) ? '1' : '0.6'
|
||||
opacity: (connected && isWalletCompatible) ? '1' : '0.6'
|
||||
}}>
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center">
|
||||
Enter URL to Deploy
|
||||
</h2>
|
||||
<URLForm
|
||||
onSubmit={handleUrlSubmit}
|
||||
disabled={!connected || !isWalletCompatible() || status === 'creating'}
|
||||
disabled={!connected || !isWalletCompatible || isFetchingChainGenesisHash || status === 'creating' || Boolean(incorrectChainWarning)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function URLForm({ onSubmit, disabled }: URLFormProps) {
|
||||
opacity: (disabled || !url) ? '0.7' : '1',
|
||||
}}
|
||||
>
|
||||
{disabled ? 'Connect Wallet First' : 'Deploy URL'}
|
||||
Deploy URL
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
|
||||
+11
-15
@@ -1,6 +1,6 @@
|
||||
import BN from 'bn.js';
|
||||
|
||||
import { Connection, ParsedInstruction, PartiallyDecodedInstruction } from '@solana/web3.js';
|
||||
import { Connection, ParsedInstruction, ParsedTransactionWithMeta, PartiallyDecodedInstruction } from '@solana/web3.js';
|
||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||
|
||||
import { getRecipientAddress } from '@/services/solana';
|
||||
@@ -8,13 +8,11 @@ import { PaymentMethod } from '../types';
|
||||
|
||||
// Extract transaction info for native GOR transfers
|
||||
const extractTxInfo = async (
|
||||
connection: Connection,
|
||||
transactionSignature: string,
|
||||
parsedTx: ParsedTransactionWithMeta,
|
||||
paymentMethod: PaymentMethod
|
||||
): Promise<{ authority: string; amount: string; destination: string }> => {
|
||||
const result = await connection.getParsedTransaction(transactionSignature, 'confirmed');
|
||||
|
||||
if (!result) {
|
||||
if (!parsedTx) {
|
||||
throw new Error('Transaction not found');
|
||||
}
|
||||
|
||||
@@ -23,7 +21,7 @@ const extractTxInfo = async (
|
||||
switch (paymentMethod) {
|
||||
case PaymentMethod.NAT_GOR:
|
||||
// Look for system program transfer instruction
|
||||
transferInstruction = result.transaction.message.instructions.find(
|
||||
transferInstruction = parsedTx.transaction.message.instructions.find(
|
||||
(instr) => 'parsed' in instr && instr.parsed.type === 'transfer'
|
||||
);
|
||||
|
||||
@@ -36,7 +34,7 @@ const extractTxInfo = async (
|
||||
|
||||
case PaymentMethod.SPL_TOKEN:
|
||||
// Look for token transfer instruction using TOKEN_PROGRAM_ID
|
||||
transferInstruction = result.transaction.message.instructions.find(
|
||||
transferInstruction = parsedTx.transaction.message.instructions.find(
|
||||
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
|
||||
);
|
||||
|
||||
@@ -68,7 +66,7 @@ const extractTxInfo = async (
|
||||
|
||||
export const verifyUnusedSolanaPayment = async (
|
||||
connection: Connection,
|
||||
transactionSignature: string,
|
||||
parsedTx: ParsedTransactionWithMeta,
|
||||
expectedAmount: BN,
|
||||
paymentMethod: PaymentMethod,
|
||||
): Promise<{
|
||||
@@ -81,9 +79,7 @@ export const verifyUnusedSolanaPayment = async (
|
||||
// TODO: Check if provided signature is already used
|
||||
|
||||
// Fetch transaction details
|
||||
const transactionResult = await connection.getParsedTransaction(transactionSignature, 'confirmed');
|
||||
|
||||
if (!transactionResult) {
|
||||
if (!parsedTx) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'Transaction not found on Solana blockchain'
|
||||
@@ -91,15 +87,15 @@ export const verifyUnusedSolanaPayment = async (
|
||||
}
|
||||
|
||||
// Check if transaction was successful
|
||||
if (transactionResult.meta?.err) {
|
||||
if (parsedTx.meta?.err) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Transaction failed: ${JSON.stringify(transactionResult.meta.err)}`
|
||||
reason: `Transaction failed: ${JSON.stringify(parsedTx.meta.err)}`
|
||||
};
|
||||
}
|
||||
|
||||
// Check transaction timestamp (5-minute window)
|
||||
const txTimestamp = transactionResult.blockTime ? new Date(transactionResult.blockTime * 1000) : null;
|
||||
const txTimestamp = parsedTx.blockTime ? new Date(parsedTx.blockTime * 1000) : null;
|
||||
if (!txTimestamp) {
|
||||
return {
|
||||
valid: false,
|
||||
@@ -119,7 +115,7 @@ export const verifyUnusedSolanaPayment = async (
|
||||
}
|
||||
|
||||
// Extract transaction info based on payment method
|
||||
const transferInfo = await extractTxInfo(connection, transactionSignature, paymentMethod);
|
||||
const transferInfo = await extractTxInfo(parsedTx, paymentMethod);
|
||||
const amount = transferInfo.amount;
|
||||
const authority = transferInfo.authority;
|
||||
const destination = transferInfo.destination;
|
||||
|
||||
Reference in New Issue
Block a user