BARU: Helius mengakuisisi Light Protocol
cara mengirim transaksi Solana menggunakan nonce tahan lama
Blog/Pengembangan

Transaksi Solana: Nonce Tahan Lama (2023)

Kepala Hubungan DeveloperHunter Davis di LinkedIn
Bacaan 6 menit

Pendahuluan

Saat mengirimkan transaksi di Solana sebagai developer, Anda perlu mengambil blockhash terbaru melalui RPC Solana. Langkah penting ini membantu mengurangi serangan replay. Setelah transaksi ditandatangani dan dikirimkan menggunakan blockhash tertentu, tidak seorang pun dapat menggunakan hash yang sama untuk memutar ulang atau mengirimkan kembali transaksi tersebut.

Bayangkan Anda perlu mengirimkan transaksi yang memerlukan tanda tangan dari cold storage offline atau hardware wallet seperti Ledger. Namun, blockhash cepat kedaluwarsa sehingga transaksi Anda berpotensi menjadi tidak valid. Di sinilah nonce tahan lama berperan untuk memungkinkan transaksi offline yang aman.

Setelah menyelesaikan panduan ini, Anda akan memahami:

  1. Apa itu nonce tahan lama.
  2. Tujuan nonce tahan lama.
  3. Cara menggunakan nonce tahan lama dalam transaksi.

Transaksi: Prasyarat

Sebelum memulai, pastikan Anda memiliki:

  • Pengetahuan dasar JavaScript.
  • NodeJS yang sudah terinstal.
  • Solana CLI yang sudah terinstal.
  • Git yang sudah terinstal

Penyiapan Lingkungan

  1. Kloning repositori contoh kami yang sudah dilengkapi utilitas:
Kode
git clone
  1. Buka folder proyek dan instal npm:
Kode
cd durable-noncenpm install
  1. Buka folder wallets di dalam folder nonce. Folder ini akan menyimpan kunci lokal untuk pengujian. Kemudian, masuk ke folder tersebut:
Kode
cd wallets
  1. Setelah Solana CLI terinstal, buat satu wallet untuk keypair pembayar:
Kode
solana-keygen new -o ./wallet.json
  1. Sekarang, tetapkan wallet ini sebagai wallet Anda di CLI untuk menerima airdrop Solana:
Kode
solana config set --keypair ./wallet.json
  1. Sekarang Anda dapat melakukan airdrop Solana ke alamat ini dengan menjalankan perintah berikut:
Kode
solana aidrop 2
  1. Kita juga perlu membuat dan mendanai wallet lain untuk otoritas nonce:
Kode
solana-keygen new -o ./nonceAuth.json
  1. Untuk public key yang dihasilkan ini, kita dapat menggunakan situs faucet untuk melakukan airdrop 1 SOL, di sini.

Setelah lingkungan siap, kita dapat melanjutkan ke langkah berikutnya.

Apa Itu Nonce Tahan Lama?

Akun nonce tahan lama di Solana dapat dianggap sebagai kotak penyimpanan aman. Saat Anda membuat akun ini, Solana memberinya kode unik dan stabil yang disebut "nonce tahan lama". Tidak seperti nonce biasa yang berubah pada setiap transaksi, nonce ini tetap sama dan berfungsi sebagai referensi yang konsisten.

Hal ini sangat berguna untuk transaksi "offline". Saat membuat transaksi, Anda merujuk nonce ini dari akun Anda. Solana memvalidasinya dengan nilai yang tersimpan. Jika nilainya cocok, transaksi akan disetujui. Dengan demikian, akun nonce tahan lama berfungsi sebagai mekanisme penyimpanan sekaligus validasi yang memastikan keaslian transaksi, sembari mengakomodasi kecepatan tinggi dan skenario offline di jaringan Solana.

Nonce tahan lama dapat digunakan dalam berbagai kasus penggunaan, seperti:

  • Transaksi terjadwal: Anda dapat mengatur transaksi agar berlangsung pada waktu tertentu di masa mendatang. Nonce tahan lama memastikan transaksi terjadwal tersebut dieksekusi dengan aman.
  • Wallet multisig: Dalam konteks wallet multisignature, nonce tahan lama menyediakan lapisan keamanan dan koordinasi tambahan di antara beberapa penanda tangan.
  • Program yang memerlukan interaksi di masa mendatang: Beberapa program di Solana perlu berinteraksi dengan program atau layanan lain pada interval tertentu. Nonce tahan lama membantu menjaga integritas interaksi tersebut.
  • Interaksi dengan blockchain lain: Saat Solana berinteraksi dengan blockchain lain, nonce tahan lama membantu memastikan validitas transaksi lintas chain.

Sekarang, kita dapat mulai membuat contoh ini.

Transaksi Solana: Langkah-Langkah Pembuatan

Langkah 1: Siapkan Dependensi dan Konstanta

Pada langkah ini, Anda akan mengimpor modul dan utilitas yang diperlukan, lalu menentukan konstanta dan keypair untuk contoh ini. Dependensi dan konstanta tersebut akan digunakan sepanjang proses transaksi.

Kode
import {
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  NonceAccount,
  NONCE_ACCOUNT_LENGTH,
  SystemProgram,
  Transaction,
} from "@solana/web3.js";
import { encodeAndWriteTransaction, loadWallet, readAndDecodeTransaction } from "./utils";

const nonceAuthKeypair = loadWallet('./wallets/nonceAuth.json');
const nonceKeypair = Keypair.generate();
const senderKeypair = loadWallet('./wallets/wallet.json');
const connection = new Connection("https://devnet.helius-rpc.com/?api-key=");
const waitTime = 120000;
const TranferAmount = LAMPORTS_PER_SOL * 0.01;

Langkah 2: Buat Fungsi sendTransaction

Fungsi sendTransaction mengatur proses pengiriman transaksi menggunakan nonce tahan lama. Fungsi ini menangani pembuatan nonce, konfirmasi, dan eksekusi transaksi.

Kode
async function sendTransaction() {
	console.log("Starting Nonce Transaction")
  try {
    const nonceCreationTxSig = await nonce();
    const confirmationStatus = await connection.confirmTransaction(nonceCreationTxSig);
    if (!confirmationStatus.value.err) {
      console.log("Nonce account creation confirmed.");

      const nonce = await getNonce();
      await createTx(nonce);
      await signOffline(waitTime);
      await executeTx();
    } else {
      console.error("Nonce account creation transaction failed:", confirmationStatus.value.err);
    }
  } catch (error) {
    console.error(error);
  }
}

Langkah 3: Buat Fungsi nonce

Fungsi nonce bertanggung jawab untuk membuat dan menginisialisasi akun nonce tahan lama. Proses ini mencakup penghitungan biaya sewa yang diperlukan untuk akun, pengambilan blockhash terbaru, serta pembuatan transaksi untuk membuat dan menginisialisasi akun nonce.

  1. Sebelum membuat akun nonce, kita perlu menghitung biaya sewa yang diperlukan untuk penyimpanan data akun dan mengambil blockhash terbaru.
Kode
async function nonce() {
  const rent = await connection.getMinimumBalanceForRentExemption(NONCE_ACCOUNT_LENGTH);
  const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
  1. Sekarang, kita akan membuat transaksi untuk membuat akun nonce. Proses ini menggunakan instruksi SystemProgram.createAccount untuk mengalokasikan ruang bagi akun nonce.
Kode
const createNonceTx = new Transaction().add(
    SystemProgram.createAccount({
      fromPubkey: nonceAuthKeypair.publicKey,
      newAccountPubkey: nonceKeypair.publicKey,
      lamports: rent,
      space: NONCE_ACCOUNT_LENGTH,
      programId: SystemProgram.programId,
    })
  );
  1. Kita akan menandatangani transaksi dengan keypair otoritas, lalu mengirimkannya ke jaringan Solana. Transaksi ini membuat akun nonce tahan lama.
Kode
createNonceTx.feePayer = nonceAuthKeypair.publicKey;
  createNonceTx.recentBlockhash = blockhash;
  createNonceTx.lastValidBlockHeight = lastValidBlockHeight;
  createNonceTx.sign(nonceAuthKeypair, nonceKeypair);

  try {
    const signature = await connection.sendRawTransaction(createNonceTx.serialize(), { skipPreflight: false, preflightCommitment: "single" });
  1. Setelah mengirimkan transaksi, kita akan mengonfirmasi statusnya untuk memastikan akun nonce berhasil dibuat.
Kode
const confirmationStatus = await connection.confirmTransaction(signature);
    if (confirmationStatus.value.err) {
      throw new Error("Nonce account creation transaction failed: " + confirmationStatus.value.err);
    }
    console.log("Nonce account created:", signature);
  1. Agar akun nonce dapat digunakan sepenuhnya, kita perlu menginisialisasi nilainya. Kita akan membuat transaksi baru untuk menjalankan instruksi SystemProgram.nonceInitialize.
Kode
// Initialize the nonce value within the account
    const initializeNonceTx = new Transaction().add(
      SystemProgram.nonceInitialize({
        noncePubkey: nonceKeypair.publicKey,
        authorizedPubkey: nonceAuthKeypair.publicKey,
      })
    );
  1. Seperti pada langkah sebelumnya, kita akan menandatangani transaksi dan mengirimkannya ke jaringan untuk menginisialisasi akun nonce.
Kode
const { blockhash: initBlockhash, lastValidBlockHeight: initLastValidBlockHeight } = await connection.getLatestBlockhash();
    initializeNonceTx.feePayer = nonceAuthKeypair.publicKey;
    initializeNonceTx.recentBlockhash = initBlockhash;
    initializeNonceTx.lastValidBlockHeight = initLastValidBlockHeight;
    initializeNonceTx.sign(nonceAuthKeypair);

    const initSignature = await connection.sendRawTransaction(initializeNonceTx.serialize(), { skipPreflight: false, preflightCommitment: "single" });
  1. Terakhir, kita akan mengonfirmasi status transaksi inisialisasi untuk memastikan akun nonce telah diinisialisasi dengan benar.
Kode
const initConfirmationStatus = await connection.confirmTransaction(initSignature);
    if (initConfirmationStatus.value.err) {
      throw new Error("Nonce initialization transaction failed: " + initConfirmationStatus.value.err);
    }
    console.log("Nonce initialized:", initSignature);
    return initSignature;
  } catch (error) {
    console.error("Failed in createNonce function: ", error);
    throw error;
  }
}

Keseluruhan fungsi akan terlihat seperti ini:

Kode
async function nonce() {
  // For creating the nonce account
  const rent = await connection.getMinimumBalanceForRentExemption(
    NONCE_ACCOUNT_LENGTH
  );
  const { blockhash, lastValidBlockHeight } =
    await connection.getLatestBlockhash();
  const createNonceTx = new Transaction().add(
    SystemProgram.createAccount({
      fromPubkey: nonceAuthKeypair.publicKey,
      newAccountPubkey: nonceKeypair.publicKey,
      lamports: rent,
      space: NONCE_ACCOUNT_LENGTH,
      programId: SystemProgram.programId,
    })
  );

  createNonceTx.feePayer = nonceAuthKeypair.publicKey;
  createNonceTx.recentBlockhash = blockhash;
  createNonceTx.lastValidBlockHeight = lastValidBlockHeight;
  createNonceTx.sign(nonceAuthKeypair, nonceKeypair);

  try {
    // Create the nonce account
    const signature = await connection.sendRawTransaction(
      createNonceTx.serialize(),
      { skipPreflight: false, preflightCommitment: "single" }
    );
    const confirmationStatus = await connection.confirmTransaction(signature);
    if (confirmationStatus.value.err) {
      throw new Error(
        "Nonce account creation transaction failed: " +
          confirmationStatus.value.err
      );
    }
    console.log("Nonce account created:", signature);

    // Now, initialize the nonce
    const initializeNonceTx = new Transaction().add(
      SystemProgram.nonceInitialize({
        noncePubkey: nonceKeypair.publicKey,
        authorizedPubkey: nonceAuthKeypair.publicKey,
      })
    );

    const {
      blockhash: initBlockhash,
      lastValidBlockHeight: initLastValidBlockHeight,
    } = await connection.getLatestBlockhash();
    initializeNonceTx.feePayer = nonceAuthKeypair.publicKey;
    initializeNonceTx.recentBlockhash = initBlockhash;
    initializeNonceTx.lastValidBlockHeight = initLastValidBlockHeight;
    initializeNonceTx.sign(nonceAuthKeypair); // Only sign with nonceAuthKeypair

    const initSignature = await connection.sendRawTransaction(
      initializeNonceTx.serialize(),
      { skipPreflight: false, preflightCommitment: "single" }
    );
    const initConfirmationStatus = await connection.confirmTransaction(
      initSignature
    );
    if (initConfirmationStatus.value.err) {
      throw new Error(
        "Nonce initialization transaction failed: " +
          initConfirmationStatus.value.err
      );
    }
    console.log("Nonce initialized:", initSignature);
    return initSignature;
  } catch (error) {
    console.error("Failed in createNonce function: ", error);
    throw error;
  }
}

Langkah 4: Buat Fungsi getNonce

Tentukan fungsi getNonce yang bertanggung jawab mengambil nilai nonce dari akun nonce yang telah dibuat.

Kode
async function getNonce() {
  const nonceAccount = await fetchNonceInfo();
  return nonceAccount.nonce;
}

Langkah 5: Buat Fungsi createTx

Tentukan fungsi createTx yang membuat contoh transaksi berisi instruksi untuk memajukan nonce dan instruksi transfer. Fungsi ini menggunakan nonce yang sebelumnya diambil untuk memastikan keaslian transaksi.

Kode
async function createTx(nonce) {
  const destination = Keypair.generate();

  const advanceNonceIx = SystemProgram.nonceAdvance({
    noncePubkey: nonceKeypair.publicKey,
    authorizedPubkey: nonceAuthKeypair.publicKey
  });

  const transferIx = SystemProgram.transfer({
    fromPubkey: senderKeypair.publicKey,
    toPubkey: destination.publicKey,
    lamports: TranferAmount,
  });

  const sampleTx = new Transaction();
  sampleTx.add(advanceNonceIx, transferIx);
  sampleTx.recentBlockhash = nonce; // Use the nonce fetched earlier
  sampleTx.feePayer = senderKeypair.publicKey;

  const serialisedTx = encodeAndWriteTransaction(sampleTx, "./unsignedTxn.json", false);
  return serialisedTx;
}

Langkah 6: Buat Fungsi signOffline

Tentukan fungsi signOffline yang bertanggung jawab menandatangani transaksi secara offline. Fungsi ini menyimulasikan penundaan offline sebelum menandatangani transaksi dengan keypair pengirim dan otoritas nonce.

Kode
async function createTx(nonce) {
  const destination = Keypair.generate();

  const advanceNonceIx = SystemProgram.nonceAdvance({
    noncePubkey: nonceKeypair.publicKey,
    authorizedPubkey: nonceAuthKeypair.publicKey
  });

  const transferIx = SystemProgram.transfer({
    fromPubkey: senderKeypair.publicKey,
    toPubkey: destination.publicKey,
    lamports: TranferAmount,
  });

  const sampleTx = new Transaction();
  sampleTx.add(advanceNonceIx, transferIx);
  sampleTx.recentBlockhash = nonce; // Use the nonce fetched earlier
  sampleTx.feePayer = senderKeypair.publicKey;

  const serialisedTx = encodeAndWriteTransaction(sampleTx, "./unsignedTxn.json", false);
  return serialisedTx;
}

Langkah 7: Buat Fungsi executeTx

Fungsi executeTx bertanggung jawab mengirimkan transaksi yang telah ditandatangani ke jaringan Solana untuk dieksekusi. Ini adalah langkah terakhir dalam proses transaksi, yaitu saat transaksi disiarkan ke jaringan.

Kode
async function executeTx() {
  const signedTx = await readAndDecodeTransaction("./signedTxn.json");
  const sig = await connection.sendRawTransaction(signedTx.serialize());
  console.log("Tx sent: ", sig);
}

Langkah 8: Buat Fungsi fetchNonceInfo

Fungsi fetchNonceInfo mengambil informasi nonce dari akun nonce yang telah dibuat dan mencoba kembali hingga tiga kali jika diperlukan. Langkah ini membantu memastikan nonce yang digunakan dalam transaksi merupakan nonce terbaru dan valid.

Kode
async function fetchNonceInfo(retries = 3) {
  while (retries > 0) {
    const accountInfo = await connection.getAccountInfo(nonceKeypair.publicKey);
    if (accountInfo) {
      const nonceAccount = NonceAccount.fromAccountData(accountInfo.data);
      return nonceAccount;
    }
    retries--;
    if (retries > 0) {
      console.log(`Retry fetching nonce in 3 seconds. ${retries} retries left.`);
      await new Promise(res => setTimeout(res, 3000)); // wait for 3 seconds
    }
  }
  throw new Error("No account info found");
}

Langkah 9: Panggil Fungsi sendTransaction

Terakhir, panggil fungsi sendTransaction untuk memulai proses transaksi. Fungsi ini menyatukan semua langkah yang telah ditentukan sebelumnya untuk membuat, menandatangani, dan mengeksekusi transaksi menggunakan nonce tahan lama.

Kode
ts-node main

Menjalankan sendTransaction akan menghasilkan tanda tangan transaksi untuk transaksi yang berhasil. Tanda tangan ini merupakan informasi penting untuk melacak dan memverifikasi transaksi di jaringan Solana.

Kode
Tx written to ./unsignedTxn.jsonTx written to ./signedTxn.jsonTx sent:  64vBuSbN8SJZo74r8KoRFF6GJD7iszdckER2NkmFfYzHCN1H9Q3iC2Z3CP7NsoAgrP2jdyQrVeSzVx6vsbxNEE5U

Anda kini telah berhasil menggunakan nonce tahan lama dalam sebuah transaksi!

Kode Lengkap

Kode
import {
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  NonceAccount,
  NONCE_ACCOUNT_LENGTH,
  SystemProgram,
  Transaction,
} from "@solana/web3.js";
import {
  encodeAndWriteTransaction,
  loadWallet,
  readAndDecodeTransaction,
} from "./utils";

const TranferAmount = LAMPORTS_PER_SOL * 0.01;

const nonceAuthKeypair = loadWallet("./wallets/nonceAuth.json");
const nonceKeypair = Keypair.generate();
const senderKeypair = loadWallet("./wallets/wallet.json");
const connection = new Connection(
  "https://devnet.helius-rpc.com/?api-key="
);

const waitTime = 120000;

async function sendTransaction() {
  try {
    // Create nonce and get its creation transaction signature
    const nonceCreationTxSig = await nonce();

    // Ensure nonce account creation is confirmed before moving forward
    const confirmationStatus = await connection.confirmTransaction(
      nonceCreationTxSig
    );
    if (!confirmationStatus.value.err) {
      console.log("Nonce account creation confirmed.");

      const nonce = await getNonce();
      await createTx(nonce);
      await signOffline(waitTime);
      await executeTx();
    } else {
      console.error(
        "Nonce account creation transaction failed:",
        confirmationStatus.value.err
      );
    }
  } catch (error) {
    console.error(error);
  }
}

async function nonce() {
  // For creating the nonce account
  const rent = await connection.getMinimumBalanceForRentExemption(
    NONCE_ACCOUNT_LENGTH
  );
  const { blockhash, lastValidBlockHeight } =
    await connection.getLatestBlockhash();
  const createNonceTx = new Transaction().add(
    SystemProgram.createAccount({
      fromPubkey: nonceAuthKeypair.publicKey,
      newAccountPubkey: nonceKeypair.publicKey,
      lamports: rent,
      space: NONCE_ACCOUNT_LENGTH,
      programId: SystemProgram.programId,
    })
  );

  createNonceTx.feePayer = nonceAuthKeypair.publicKey;
  createNonceTx.recentBlockhash = blockhash;
  createNonceTx.lastValidBlockHeight = lastValidBlockHeight;
  createNonceTx.sign(nonceAuthKeypair, nonceKeypair);

  try {
    // Create the nonce account
    const signature = await connection.sendRawTransaction(
      createNonceTx.serialize(),
      { skipPreflight: false, preflightCommitment: "single" }
    );
    const confirmationStatus = await connection.confirmTransaction(signature);
    if (confirmationStatus.value.err) {
      throw new Error(
        "Nonce account creation transaction failed: " +
          confirmationStatus.value.err
      );
    }
    console.log("Nonce account created:", signature);

    // Now, initialize the nonce
    const initializeNonceTx = new Transaction().add(
      SystemProgram.nonceInitialize({
        noncePubkey: nonceKeypair.publicKey,
        authorizedPubkey: nonceAuthKeypair.publicKey,
      })
    );

    const {
      blockhash: initBlockhash,
      lastValidBlockHeight: initLastValidBlockHeight,
    } = await connection.getLatestBlockhash();
    initializeNonceTx.feePayer = nonceAuthKeypair.publicKey;
    initializeNonceTx.recentBlockhash = initBlockhash;
    initializeNonceTx.lastValidBlockHeight = initLastValidBlockHeight;
    initializeNonceTx.sign(nonceAuthKeypair); // Only sign with nonceAuthKeypair

    const initSignature = await connection.sendRawTransaction(
      initializeNonceTx.serialize(),
      { skipPreflight: false, preflightCommitment: "single" }
    );
    const initConfirmationStatus = await connection.confirmTransaction(
      initSignature
    );
    if (initConfirmationStatus.value.err) {
      throw new Error(
        "Nonce initialization transaction failed: " +
          initConfirmationStatus.value.err
      );
    }
    console.log("Nonce initialized:", initSignature);
    return initSignature;
  } catch (error) {
    console.error("Failed in createNonce function: ", error);
    throw error;
  }
}

async function getNonce() {
  const nonceAccount = await fetchNonceInfo();
  return nonceAccount.nonce;
}

async function createTx(nonce: string) {
  const destination = Keypair.generate();

  const advanceNonceIx = SystemProgram.nonceAdvance({
    noncePubkey: nonceKeypair.publicKey,
    authorizedPubkey: nonceAuthKeypair.publicKey,
  });

  const transferIx = SystemProgram.transfer({
    fromPubkey: senderKeypair.publicKey,
    toPubkey: destination.publicKey,
    lamports: TranferAmount,
  });

  const sampleTx = new Transaction();
  sampleTx.add(advanceNonceIx, transferIx);
  sampleTx.recentBlockhash = nonce; // Use the nonce fetched earlier
  sampleTx.feePayer = senderKeypair.publicKey;

  const serialisedTx = encodeAndWriteTransaction(
    sampleTx,
    "./unsigned.json",
    false
  );
  return serialisedTx;
}
async function signOffline(waitTime = 120000): Promise {
  await new Promise((resolve) => setTimeout(resolve, waitTime));
  const unsignedTx = readAndDecodeTransaction("./unsigned.json");
  unsignedTx.sign(senderKeypair, nonceAuthKeypair); // Sign with both keys
  const serialisedTx = encodeAndWriteTransaction(unsignedTx, "./signed.json");
  return serialisedTx;
}

async function executeTx() {
  const signedTx = readAndDecodeTransaction("./signed.json");
  const sig = await connection.sendRawTransaction(signedTx.serialize());
  console.log("      Tx sent: ", sig);
}

async function fetchNonceInfo(retries = 3): Promise {
  while (retries > 0) {
    const accountInfo = await connection.getAccountInfo(nonceKeypair.publicKey);
    if (accountInfo) {
      const nonceAccount = NonceAccount.fromAccountData(accountInfo.data);
      return nonceAccount;
    }
    retries--;
    if (retries > 0) {
      console.log(
        `Retry fetching nonce in 3 seconds. ${retries} retries left.`
      );
      await new Promise((res) => setTimeout(res, 3000)); // wait for 3 seconds
    }
  }
  throw new Error("No account info found");
}

sendTransaction();

Transaksi Solana: Menggunakan RPC Helius

Helius dapat berfungsi sebagai perantara andal untuk berinteraksi dengan RPC Solana, sehingga menyederhanakan proses pengambilan informasi blockhash yang diperlukan untuk nonce tahan lama. Melalui Helius, Anda dapat mengelola siklus hidup transaksi Solana dengan lebih andal, terutama untuk skenario offline. Helius menyediakan akses yang lebih ringkas ke blockhash, sehingga membantu developer membuat aplikasi yang lebih tangguh terhadap transaksi yang kedaluwarsa.

Singkatnya, nonce tahan lama dalam transaksi Solana menawarkan cara yang aman dan andal untuk menangani transaksi offline serta memastikan keaslian transaksi. Dengan mengikuti langkah-langkah dalam panduan ini, developer dapat menerapkan nonce tahan lama pada aplikasi Solana mereka untuk meningkatkan keamanan dan fleksibilitas.

‍

‍

‍

Berlangganan Helius

Ikuti perkembangan terbaru dalam pengembangan Solana dan dapatkan pembaruan saat kami memublikasikan postingan