Compare commits

..

1 Commits

Author SHA1 Message Date
2ba3e9db97 Update deployment for MTM tokens 2025-07-26 18:21:45 +05:30
8 changed files with 50 additions and 87 deletions

View File

@ -3,8 +3,8 @@
# Solana Payment Configuration # Solana Payment Configuration
# TODO: Use different RPC URL # TODO: Use different RPC URL
NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158 NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158
NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=97RggLo3zV5kFGYW4yoQTxr4Xkz4Vg2WPHzNYXXWpump
NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL=GOR NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL=MTM
# Gorbagana Chain Configuration # Gorbagana Chain Configuration
NEXT_PUBLIC_GORBAGANA_RPC_URL=https://rpc.gorbagana.wtf NEXT_PUBLIC_GORBAGANA_RPC_URL=https://rpc.gorbagana.wtf
@ -23,8 +23,8 @@ NEXT_PUBLIC_REGISTRY_GQL_ENDPOINT=https://laconicd-mainnet-1.laconic.com/graphql
NEXT_PUBLIC_ALNT_COST_LRN=lrn://laconic/pricing/alnt NEXT_PUBLIC_ALNT_COST_LRN=lrn://laconic/pricing/alnt
NEXT_PUBLIC_DEPLOYMENT_COST_LRN=lrn://laconic/pricing/webapp-deployment NEXT_PUBLIC_DEPLOYMENT_COST_LRN=lrn://laconic/pricing/webapp-deployment
REGISTRY_GAS_PRICE=0.001 REGISTRY_GAS_PRICE=0.001
REGISTRY_AUTHORITY=laconic-deploy
REGISTRY_BOND_ID=5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2 REGISTRY_BOND_ID=5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2
REGISTRY_AUTHORITY=laconic-deploy
REGISTRY_USER_KEY= REGISTRY_USER_KEY=
# Application Configuration # Application Configuration

View File

@ -55,7 +55,7 @@
- Add configuration for the app: - Add configuration for the app:
```bash ```bash
curl -s https://git.vdb.to/LaconicNetwork/gor-deploy/raw/branch/main/.env.example -o .app.env curl -s https://git.vdb.to/LaconicNetwork/gor-deploy/raw/branch/mtm-deploy/.env.example -o .app.env
# Fill in the required values # Fill in the required values
nano .app.env nano .app.env
@ -87,7 +87,7 @@
# In gor-deploy/deploy dir # In gor-deploy/deploy dir
docker run -it \ docker run -it \
-v ./:/app/deploy -w /app/deploy \ -v ./:/app/deploy -w /app/deploy \
-e DEPLOYMENT_DNS=gor-deploy.laconic.com \ -e DEPLOYMENT_DNS=mtm-deploy.laconic.com \
cerc/laconic-registry-cli:latest \ cerc/laconic-registry-cli:latest \
./deploy.sh ./deploy.sh
``` ```

View File

@ -12,12 +12,12 @@ echo "Using AUTHORITY: $AUTHORITY"
REPO_URL="https://git.vdb.to/LaconicNetwork/gor-deploy" REPO_URL="https://git.vdb.to/LaconicNetwork/gor-deploy"
# Get the latest commit hash for a branch # Get the latest commit hash for a branch
BRANCH_NAME="main" BRANCH_NAME="mtm-deploy"
LATEST_HASH=$(git ls-remote $REPO_URL refs/heads/$BRANCH_NAME | awk '{print $1}') LATEST_HASH=$(git ls-remote $REPO_URL refs/heads/$BRANCH_NAME | awk '{print $1}')
PACKAGE_VERSION=$(curl -s $REPO_URL/raw/branch/$BRANCH_NAME/package.json | jq -r .version) PACKAGE_VERSION=$(curl -s $REPO_URL/raw/branch/$BRANCH_NAME/package.json | jq -r .version)
APP_NAME=gor-deploy APP_NAME=mtm-deploy
echo "Repo: ${REPO_URL}" echo "Repo: ${REPO_URL}"
echo "Latest hash: ${LATEST_HASH}" echo "Latest hash: ${LATEST_HASH}"

View File

@ -1,8 +1,8 @@
record: record:
type: ApplicationRecord type: ApplicationRecord
version: 0.0.4 version: 0.0.3
repository_ref: bc0d10d4c38d2d20de88ac8352ab8571bbbffef4 repository_ref: 169c992abfca6994f78ba1b37049f47d33451040
repository: ["https://git.vdb.to/LaconicNetwork/gor-deploy"] repository: ["https://git.vdb.to/LaconicNetwork/gor-deploy"]
app_type: webapp app_type: webapp
name: gor-deploy name: mtm-deploy
app_version: 0.1.0 app_version: 0.1.0

View File

@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from 'next/server';
import axios from 'axios'; import axios from 'axios';
import assert from 'assert'; import assert from 'assert';
import { Connection, ParsedTransactionWithMeta } from '@solana/web3.js'; import { Connection } from '@solana/web3.js';
import { verifyUnusedSolanaPayment } from '@/utils/solana-verify'; import { verifyUnusedSolanaPayment } from '@/utils/solana-verify';
import { transferLNTTokens } from '@/services/laconic-transfer'; import { transferLNTTokens } from '@/services/laconic-transfer';
@ -25,7 +25,6 @@ const ALLOWED_SLIPPAGE_FACTOR = 0.2
// Use CAIP convention for chain ID: namespace + reference // Use CAIP convention for chain ID: namespace + reference
const SOLANA_CHAIN_ID = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; // Solana mainnet 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 // Sleep helper function
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
@ -168,7 +167,6 @@ export async function POST(request: NextRequest) {
// First check if the request body is valid JSON // First check if the request body is valid JSON
let url, txHash, senderPublicKey, paymentMethod; let url, txHash, senderPublicKey, paymentMethod;
let connection: Connection; let connection: Connection;
let parsedTx: ParsedTransactionWithMeta | null;
try { try {
const body = await request.json(); const body = await request.json();
@ -188,8 +186,6 @@ export async function POST(request: NextRequest) {
}, { status: 400 }); }, { status: 400 });
} }
parsedTx = tx;
const signerKeys = tx.transaction.message.accountKeys const signerKeys = tx.transaction.message.accountKeys
.filter(k => k.signer) .filter(k => k.signer)
.map(k => k.pubkey.toBase58()); .map(k => k.pubkey.toBase58());
@ -247,13 +243,9 @@ export async function POST(request: NextRequest) {
const requiredAmountInBaseUnits = requiredTokenInfo.requiredAmountInBaseUnits; const requiredAmountInBaseUnits = requiredTokenInfo.requiredAmountInBaseUnits;
const expectedTokenAmount = Math.round(requiredAmountInBaseUnits - ALLOWED_SLIPPAGE_FACTOR * 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( const solanaPaymentResult = await verifyUnusedSolanaPayment(
connection, connection,
parsedTx, txHash,
new BN(expectedTokenAmount), new BN(expectedTokenAmount),
paymentMethod, paymentMethod,
); );
@ -465,7 +457,7 @@ export async function POST(request: NextRequest) {
repository_ref: fullHash, repository_ref: fullHash,
}, },
external_payment: { external_payment: {
chain_id: paymentMethod === PaymentMethod.SPL_TOKEN ? SOLANA_CHAIN_ID : GORBAGANA_CHAIN_ID, chain_id: SOLANA_CHAIN_ID,
tx_hash: txHash, tx_hash: txHash,
pubkey: senderPublicKey pubkey: senderPublicKey
}, },

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect, useState, useRef, useMemo } from 'react'; import { useCallback, useEffect, useState, useRef } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
@ -27,14 +27,6 @@ const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: f
// RPC endpoint reference: https://docs.gorbagana.wtf/testnet-v2-devnet.html // RPC endpoint reference: https://docs.gorbagana.wtf/testnet-v2-devnet.html
const GORBAGANA_GENESIS_HASH = '533uBE9RRquhTBqEX58oV52FdTTsReMdAvaUvP6hNjsn'; 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() { export default function Home() {
const { wallet, connected, publicKey, disconnect } = useWallet(); const { wallet, connected, publicKey, disconnect } = useWallet();
@ -50,8 +42,7 @@ export default function Home() {
const [appName, setAppName] = useState<string | null>(null); const [appName, setAppName] = useState<string | null>(null);
const [repoUrl, setRepoUrl] = useState<string | null>(null); const [repoUrl, setRepoUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [incorrectChainWarning, setIncorrectChainWarning] = useState<string | null>(null); const [incorrectChainWarining, setIncorrectChainWarining] = useState<string | null>(null);
const [isFetchingChainGenesisHash, setIsFetchingChainGenesisHash] = useState(false);
useEffect(() => { useEffect(() => {
if (!IS_NAT_GOR_TRANSFER_ENABLED) { if (!IS_NAT_GOR_TRANSFER_ENABLED) {
@ -60,38 +51,20 @@ export default function Home() {
}, [setSelectedPaymentMethod]); }, [setSelectedPaymentMethod]);
useEffect(() => { useEffect(() => {
setIncorrectChainWarning(null); if (!wallet || wallet.adapter.name !== BackpackWalletName || selectedPaymentMethod !== PaymentMethod.NAT_GOR) {
if (!wallet || wallet.adapter.name !== BackpackWalletName || !connected || !selectedPaymentMethod) {
return; return;
} }
const warnOnIncorrectChain = async () => { const warnOnIncorrectChain = async () => {
setIsFetchingChainGenesisHash(true);
try {
// @ts-expect-error: backpack exists on window object // @ts-expect-error: backpack exists on window object
const genesisHash = await window.backpack.solana.connection.getGenesisHash(); const genesisHash = await window.backpack.solana.connection.getGenesisHash();
if (genesisHash !== GORBAGANA_GENESIS_HASH) {
const expectedGenesisHash = selectedPaymentMethod === PaymentMethod.NAT_GOR setIncorrectChainWarining("WARNING: Unsupported chain selected in wallet. Please switch to Gorbagana chain")
? 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(); warnOnIncorrectChain();
}, [wallet, selectedPaymentMethod, connected]); }, [wallet, selectedPaymentMethod]);
// Track previous payment method to detect switches // Track previous payment method to detect switches
const previousPaymentMethodRef = useRef<PaymentMethod | null>(null); const previousPaymentMethodRef = useRef<PaymentMethod | null>(null);
@ -117,7 +90,7 @@ export default function Home() {
}; };
// Helper function to check if current wallet is compatible with selected payment method // Helper function to check if current wallet is compatible with selected payment method
const isWalletCompatible = useMemo(() => { const isWalletCompatible = () => {
if (!selectedPaymentMethod || !wallet) return false; if (!selectedPaymentMethod || !wallet) return false;
const walletName = wallet.adapter.name.toLowerCase(); const walletName = wallet.adapter.name.toLowerCase();
@ -126,9 +99,9 @@ export default function Home() {
if (selectedPaymentMethod === PaymentMethod.NAT_GOR) { if (selectedPaymentMethod === PaymentMethod.NAT_GOR) {
return isBackpack; // Only Backpack for native GOR return isBackpack; // Only Backpack for native GOR
} else { } else {
return true; // Only non-Backpack wallets for SPL tokens return !isBackpack; // Only non-Backpack wallets for SPL tokens
} }
}, [selectedPaymentMethod, wallet]); };
const handlePaymentComplete = useCallback(async (hash: string, paymentMethod: PaymentMethod) => { const handlePaymentComplete = useCallback(async (hash: string, paymentMethod: PaymentMethod) => {
if (!publicKey || !url) { if (!publicKey || !url) {
@ -220,7 +193,7 @@ export default function Home() {
<div className="text-left"> <div className="text-left">
<h3 className="font-semibold text-lg mb-2">{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} Token</h3> <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)' }}> <p className="text-xs mt-1" style={{ color: 'var(--muted-foreground)' }}>
Compatible with: All Solana compatible wallets Compatible with: Phantom, Solflare
</p> </p>
</div> </div>
</button> </button>
@ -252,28 +225,23 @@ export default function Home() {
<div className="flex flex-col items-center space-y-3"> <div className="flex flex-col items-center space-y-3">
<div className="flex items-center"> <div className="flex items-center">
<span className="w-3 h-3 rounded-full mr-2" style={{ <span className="w-3 h-3 rounded-full mr-2" style={{
backgroundColor: isWalletCompatible ? 'var(--success)' : 'var(--destructive)' backgroundColor: isWalletCompatible() ? 'var(--success)' : 'var(--destructive)'
}}></span> }}></span>
<p className="font-medium" style={{ <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> </p>
</div> </div>
{!isWalletCompatible && ( {!isWalletCompatible() && (
<p className="text-sm text-amber-400"> <p className="text-sm text-amber-400">
This wallet is not compatible with {PAYMENT_METHOD_LABELS[selectedPaymentMethod]} payments. This wallet is not compatible with {PAYMENT_METHOD_LABELS[selectedPaymentMethod]} payments.
Please select a different wallet or payment method. Please select a different wallet or payment method.
</p> </p>
)} )}
{ isFetchingChainGenesisHash && ( {incorrectChainWarining && (
<p className="text-sm text-muted-foreground">
Checking wallet network compatibility...
</p>
)}
{incorrectChainWarning && (
<p className="text-sm text-amber-400"> <p className="text-sm text-amber-400">
{incorrectChainWarning} {incorrectChainWarining}
</p> </p>
)} )}
<div className="w-full p-3 rounded-md" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}> <div className="w-full p-3 rounded-md" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
@ -308,19 +276,18 @@ export default function Home() {
</div> </div>
)} )}
</div> </div>
{/* Step 3: URL Input */} {/* Step 3: URL Input */}
<div className="mb-8 p-6 rounded-lg" style={{ <div className="mb-8 p-6 rounded-lg" style={{
background: 'var(--muted-light)', background: 'var(--muted-light)',
borderLeft: '4px solid var(--primary)', 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"> <h2 className="text-lg font-semibold mb-4 flex items-center">
Enter URL to Deploy Enter URL to Deploy
</h2> </h2>
<URLForm <URLForm
onSubmit={handleUrlSubmit} onSubmit={handleUrlSubmit}
disabled={!connected || !isWalletCompatible || isFetchingChainGenesisHash || status === 'creating' || Boolean(incorrectChainWarning)} disabled={!connected || !isWalletCompatible() || status === 'creating'}
/> />
</div> </div>

View File

@ -103,7 +103,7 @@ export default function URLForm({ onSubmit, disabled }: URLFormProps) {
opacity: (disabled || !url) ? '0.7' : '1', opacity: (disabled || !url) ? '0.7' : '1',
}} }}
> >
Deploy URL {disabled ? 'Connect Wallet First' : 'Deploy URL'}
</button> </button>
</form> </form>
); );

View File

@ -1,6 +1,6 @@
import BN from 'bn.js'; import BN from 'bn.js';
import { Connection, ParsedInstruction, ParsedTransactionWithMeta, PartiallyDecodedInstruction } from '@solana/web3.js'; import { Connection, ParsedInstruction, PartiallyDecodedInstruction } from '@solana/web3.js';
import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
import { getRecipientAddress } from '@/services/solana'; import { getRecipientAddress } from '@/services/solana';
@ -8,11 +8,13 @@ import { PaymentMethod } from '../types';
// Extract transaction info for native GOR transfers // Extract transaction info for native GOR transfers
const extractTxInfo = async ( const extractTxInfo = async (
parsedTx: ParsedTransactionWithMeta, connection: Connection,
transactionSignature: string,
paymentMethod: PaymentMethod paymentMethod: PaymentMethod
): Promise<{ authority: string; amount: string; destination: string }> => { ): Promise<{ authority: string; amount: string; destination: string }> => {
const result = await connection.getParsedTransaction(transactionSignature, 'confirmed');
if (!parsedTx) { if (!result) {
throw new Error('Transaction not found'); throw new Error('Transaction not found');
} }
@ -21,7 +23,7 @@ const extractTxInfo = async (
switch (paymentMethod) { switch (paymentMethod) {
case PaymentMethod.NAT_GOR: case PaymentMethod.NAT_GOR:
// Look for system program transfer instruction // Look for system program transfer instruction
transferInstruction = parsedTx.transaction.message.instructions.find( transferInstruction = result.transaction.message.instructions.find(
(instr) => 'parsed' in instr && instr.parsed.type === 'transfer' (instr) => 'parsed' in instr && instr.parsed.type === 'transfer'
); );
@ -34,7 +36,7 @@ const extractTxInfo = async (
case PaymentMethod.SPL_TOKEN: case PaymentMethod.SPL_TOKEN:
// Look for token transfer instruction using TOKEN_PROGRAM_ID // Look for token transfer instruction using TOKEN_PROGRAM_ID
transferInstruction = parsedTx.transaction.message.instructions.find( transferInstruction = result.transaction.message.instructions.find(
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID) (instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
); );
@ -66,7 +68,7 @@ const extractTxInfo = async (
export const verifyUnusedSolanaPayment = async ( export const verifyUnusedSolanaPayment = async (
connection: Connection, connection: Connection,
parsedTx: ParsedTransactionWithMeta, transactionSignature: string,
expectedAmount: BN, expectedAmount: BN,
paymentMethod: PaymentMethod, paymentMethod: PaymentMethod,
): Promise<{ ): Promise<{
@ -79,7 +81,9 @@ export const verifyUnusedSolanaPayment = async (
// TODO: Check if provided signature is already used // TODO: Check if provided signature is already used
// Fetch transaction details // Fetch transaction details
if (!parsedTx) { const transactionResult = await connection.getParsedTransaction(transactionSignature, 'confirmed');
if (!transactionResult) {
return { return {
valid: false, valid: false,
reason: 'Transaction not found on Solana blockchain' reason: 'Transaction not found on Solana blockchain'
@ -87,15 +91,15 @@ export const verifyUnusedSolanaPayment = async (
} }
// Check if transaction was successful // Check if transaction was successful
if (parsedTx.meta?.err) { if (transactionResult.meta?.err) {
return { return {
valid: false, valid: false,
reason: `Transaction failed: ${JSON.stringify(parsedTx.meta.err)}` reason: `Transaction failed: ${JSON.stringify(transactionResult.meta.err)}`
}; };
} }
// Check transaction timestamp (5-minute window) // Check transaction timestamp (5-minute window)
const txTimestamp = parsedTx.blockTime ? new Date(parsedTx.blockTime * 1000) : null; const txTimestamp = transactionResult.blockTime ? new Date(transactionResult.blockTime * 1000) : null;
if (!txTimestamp) { if (!txTimestamp) {
return { return {
valid: false, valid: false,
@ -115,7 +119,7 @@ export const verifyUnusedSolanaPayment = async (
} }
// Extract transaction info based on payment method // Extract transaction info based on payment method
const transferInfo = await extractTxInfo(parsedTx, paymentMethod); const transferInfo = await extractTxInfo(connection, transactionSignature, paymentMethod);
const amount = transferInfo.amount; const amount = transferInfo.amount;
const authority = transferInfo.authority; const authority = transferInfo.authority;
const destination = transferInfo.destination; const destination = transferInfo.destination;