LapTrinhBlockchain

Chia sẻ kiến thức về Lập Trình Blockchain

Lập trình Blockchain, Lập Trình DApp, Lập trình NodeJs

Hướng dẫn lập trình NodeJs kết nối tới Solana Blockchain

Hướng dẫn lập trình NodeJs kết nối tới Solana Blockchain

Hướng dẫn lập trình NodeJs kết nối tới Solana Blockchain

Chia sẻ bài viết
5
(70)

Hiện tại Solana Blockchain là network hiện tại đang được đánh giá là blockchain có tốc độ nhanh nhất, và Solana được cộng đồng chú ý nhiều khi đồng tiền SOL của hệ sinh thái Solana đã có sự tăng trưởng ngoạn mục từ 1$ lên đến trên 200$. Vì thế có nhiều DEV quan tâm tới hệ sinh thái Solana này và mình cũng vậy. Trong bài viết này mình muốn chia sẻ lập trình kết nối Solana Network sử dụng NodeJs.

Gần đây do nhu cầu công việc, mình cần kết nối tới Solana network, nhưng do mới bắt đầu tìm hiểu thấy có vẻ khó và phức tạp hơn so với network khác. Mình đã từng lập trình NodeJs để kết nối tới các network: Binance Smart Chain (BSC), Heco, Polygon Tron, nhưng với Solana thì khác hẳn, do đó mình mất khá nhiều thời gian tìm hiểu. Vì thế mình chia sẻ những cái mình đã tìm hiểu được để hy vọng giúp được các DEV khác tiết kiệm được thời gian tìm hiểu, từ đó nâng cao tiến độ công việc. Toàn bộ mã nguồn mình đều public trên Github: Solana Examples in NodeJs

Một số thư viện sử dụng

Đối tượng Web3 Connection

Mọi người có thể dùng trực tiếp thư viện HTTP để gọi thông qua JSON RPC API của Solana. Nhưng để giảm thời gian và công sức mình sử dụng thư viện solana-web3 trên NodeJs để kết nối tới Solana Blockchain. Chi tiết cài đặt có thê xem trực tiếp trên solana-web3.

Để truy vấn lên Solana Blockchain chúng ta phải tạo đổi tượng Solana Web3 Connection:

let connection = new web3.Connection(
    web3.clusterApiUrl("mainnet-beta"),
    "confirmed",
);

Hoặc:

let connection = new web3.Connection(
    "https://api.mainnet-beta.solana.com",
    "confirmed",
);

Về địa chỉ public node của Solana bạn có thể xem tại: Solana Cluster RPC Endpoints.

Về các API của web3 bạn xem tại:

Sollet Wallet

Sollet Wallet là ví đầu tiên trên Solana, được xây dựng bởi các Dev bên dự án Serum, nên được hầu hết các DApp trên Solana hỗ trợ. Ví này hoàn toàn Open Source, vì thế mình có tham khảo để biết các tạo địa chỉ ví và cách lấy tài khoản SOL và các token trên Solana. Bạn xem mã nguồn tại địa chỉ : SPL Token Wallet.

Raydium Frontend

Raydium là AMM đầu tiên trên Solana với khối lượng giao dịch đứng vào hàng top, đặc biệt lai có nền tảng IDO như AcceleRaytor, DropZone nên được cộng đồng khá chú ý.

Raydium có public source code của phần Frontend tại địa chỉ Raydium UI Github, Mình tham khảo mã nguồn này để biết cách lấy giá của token và làm thế nào để thực hiện lệnh swap.

Orca Typescript SDK

Orca Typescript SDK là thư viện trên NodeJs giúp các DEV dễ dàng tương tác với Orca. Bạn tham khảo source code để biết các tính giá đặc biệt là cho các cặp Stable coin và cách thực hiện swap trên Orca.

Thư viện khác

Ngoài ra sử dụng thêm một số thư viện khác:

  • minimist: Để parse tham số đầu vào, giúp hỗ trợ nhiều tham số linh động hơn
  • bip39: Sinh chuỗi kí tự để giúp tạo ví.

Các trang web sử dụng

  • Solscan: Tra cứu giao dịch và ví
  • Sollet: Tạo ví solana đơn giản và dễ dàng, không cần phải cài đặt gì thêm.

Một số ví dụ

Toàn bộ mã nguồn trong bài viết này mình đều public trên Github: Solana Examples in NodeJs. Mình cố gắng rút gọn source code tới mức tối thiểu, mình số chỗ mình hard code để giúp người mới xem sẽ dễ hiểu hơn. Trước khi chạy các ví dụ, các bạn nhớ cài đặt các thư viện trước bằng lệnh sau:

npm install

Ví dụ 01: Lập trình tạo một ví mới trên Solana

Cũng giống như trên các Blockchain khác, bạn có hai cách để tạo ví:

  • C1: Bạn sinh ngẫu nhiên một mảng 32 byte bất kỳ để làm Private Key. Trên Solana Web3 có hỗ trợ hàm Web3.Keypair.generate() để thực hiện việc này.
  • C2: Tạo HD Wallet (Hierarchical Deterministic Wallet) bằng cách sinh ra cụm kí tự gợi nhớ (Mnemonic Seed Phrase) sử dụng thư viện BIP39. Có ba DERIVATION PATH đã được sử dụng:
    • m/44’/501’/0’/0′ => Sollet đang sử dụng mặc định
    • m/44’/501’/0′ => Được Phantom sử dụng
    • m/501’/0’/0/0 => Được Sollet sử dụng ở phiên bản đầu, sau này không sử dụng nữa.

Một số code liên quan:

// Generate wallet
async function generateWallet() {
    let mnemonic = bip39.generateMnemonic(256);
    let seed = await bip39.mnemonicToSeed(mnemonic);
    let privateKey = derivePath(DERIVATION_PATH, seed).key;
    let account = Web3.Keypair.fromSeed(new Uint8Array(privateKey));
    console.log("Mnemonic:", mnemonic);
    console.log("Private Key:", privateKey.toString('hex'));
    console.log("Private Key:", "[" + privateKey.join(",") + "]");
    console.log("Wallet:", account.publicKey.toBase58());
    return true;
}

async function generateAccount() {
    let account = Web3.Keypair.generate();
    let privateKey = Buffer.from(account.secretKey).slice(0, 32);
    console.log("Private Key:", privateKey.toString('hex', 0, 32));
    console.log("Private Key:", "[" + privateKey.join(",") + "]");
    console.log("Wallet:", account.publicKey.toBase58());
    return true;
}

Chi tiết source code bạn xem tại tệp account/create-wallet.js. Trong source code mình có viết thêm các hàm để kiểm tra thông tin ví từ 1 chuỗi kí tự gợi nhớ hay từ Private key. Một số lệnh hỗ trợ như sau:

// Xem các lệnh hỗ trợ
node account/create-wallet.js --help

// Tạo ví mới thông qua tạo ngẫu nhiên cụm kí tự gợi nhớ
node account/create-wallet.js --action=generate --type=mnemonic

// Tạo ví mới từ việc sinh ngẫn nhiên Private key
node account/create-wallet.js --action=generate --type=account

// Kiểm tra thông tin ví từ cụm kí tự gợi nhớ
node account/create-wallet.js --action=check --type=mnemonic --mnemonic="xxxxxx"

// Kiểm tra thông tin ví từ Private key
node account/create-wallet.js --action=check --type=privatekey --privateKey=xxxxxx
Ví dụ chạy một số lệnh sinh ví trên Solana
Ví dụ chạy một số lệnh sinh ví trên Solana

Ví dụ 2: Kiểm tra số dư SOL trong một địa chỉ ví

Phần này không khó, mọi người có thể tìm khá nhiều ví dụ trên mạng. Đơn giản nhất bạn chỉ cần gọi hàm getBalance() của đối tượng Web3.Connection của thư viện @solana/web3.js.

Một số code liên quan:

// Show solana balance
async function showSolBalance() {
    let connection = getConnection();
    let accountAddr = accountAddress;
    let account = new web3.PublicKey(accountAddr);
    let accountBalance = await connection.getBalance(account);
    let balance = (accountBalance/10**SOLANA_DECIMAL).toFixed(6);
    console.log(`Balance of account ${accountAddr}: ${balance} SOL`);
}

Chi tiết source code bạn xem tại tệp sol/solana-balance.js. Một số lệnh hỗ trợ như sau:

// Xem các lệnh hỗ trợ
node sol/solana-balance.js --help

// Tra số dư SOL của địa chỉ ví mặc định trên mainnet: Y2akr3bXHRsqyP1QJtbm9G9N88ZV4t1KfaFeDzKRTfr
node sol/solana-balance.js

// Tra số dư SOL của địa chỉ ví trên mainnet
node sol/solana-balance.js --accountAddress=DSRVdh9PQaqAcFtMCbJhyD4yMD5H2EeHNzdbqWctRY4E

// Tra số dư một địa chỉ ví trên testnet testnet
node sol/solana-balance.js --nodeType=testnet --accountAddress=Hhm3FxpmpRZgsQmqS4FBALTTCHaTPZ47AJJzp19ZomfZ
Ví dụ chạy một số lệnh kiểm tra số dư SOL trên một số ví
Ví dụ chạy một số lệnh kiểm tra số dư SOL trên một số ví

Ví dụ 3: Chuyển SOL giữa các ví trên Solana

Để chuyển SOL giữa các ví Solana, đơn giản nhất bạn sử dụng Web3 theo cách bước như sau:

  • Bước 1: Tạo 1 transaction và add transfer instruction: web3.SystemProgram.transfer()
  • Bước 2: Gọi hàm sendAndConfirmTransaction() của web3.

Dưới đây là phần code chính liên quan:

// Transfer solana between accounts
async function transferSOL(fromPrivateKey, toAddress, amount) {
    let connection = getConnection();
    let fromAccount = web3.Keypair.fromSecretKey(new Uint8Array(Buffer.from(fromPrivateKey, "hex")));
    let toAccount = new web3.PublicKey(toAddress);
    let lamports = (amount*web3.LAMPORTS_PER_SOL).toFixed(0);

    // Add transfer instruction to transaction
    var transaction = new web3.Transaction().add(
        web3.SystemProgram.transfer({
            fromPubkey: fromAccount.publicKey,
            toPubkey: toAccount,
            lamports: lamports,
        })
    );

    // Sign transaction, broadcast, and confirm
    var signature = await web3.sendAndConfirmTransaction(
        connection,
        transaction,
        [fromAccount]
    );
    console.log("Response", signature);         // Signature is the transaction hash
}

Toàn bộ source code được đóng gói trong tệp sol/solana-transfer.js.

Một số lệnh tham khảo:

// Xem các lệnh hỗ trợ
node sol/solana-balance.js --help

// Claim SOL trên Testnet để test
node sol/solana-transfer.js --type=airdrop --address=5W767fcieKYMDKpPYn7TGVDQpMPpFGMUHFMSqeap43Wa

// Transfer SOL trên Testnet
node sol/solana-transfer.js --type=transfer --privateKey=1b5f9912206718b36d230bb93fcee722dcae707bb44270821e39df7bdeb6c54a --toAddress=XDC4iNqUJi6WAWXTwEFGo5z1AHzEVbMdB8mGi58jKsD --amount=0.001

// Transfer SOL trên Mainnet
node sol/solana-transfer.js --nodeType=mainnet-beta --type=transfer --privateKey=<xxxx> --toAddress=XDC4iNqUJi6WAWXTwEFGo5z1AHzEVbMdB8mGi58jKsD --amount=0.001
Ví dụ chạy 1 số lệnh transfer SOL
Ví dụ chạy 1 số lệnh transfer SOL

Ví dụ 4: Lấy số dư các SPL Token trong một ví

Lúc đầu cứ nghĩ đơn giản, nhưng sau một hồi loay hoay thử các source code trên mạng thì không có cái nào đúng ý mình. Nếu trên BSC/Ethereum đơn giản chỉ cần gọi hàm balanceOf() của contract tương ứng với token, nhưng trên Solana thì khác hoàn toàn. Mất nhiều thời gian tìm source code trên mạng không thành công, cuối cùng tôi phải tìm giải pháp khác, đó là đọc source code của Sollet (Một ví đầu tiên của Solana) tại địa chỉ: https://github.com/project-serum/spl-token-wallet

Nhờ đọc source code của Sollet, tôi biết cách lấy số dư các token trong ví. Solana cho phép lấy toàn bộ thông tin tất cả các token có trong ví. Các bước cơ bản như sau:

  • Gọi hàm này connection.getProgramAccounts() để lấy các token accounts liên quan tới ví.
  • Dùng thư viện buffer-layout để parse dữ liệu trả về để thu được thông tin token.
  • Filter để lấy đúng token ta cần.

Trong phần này ngoài thư viện @solana/web3.js, chúng ta cần dùng thêm hai thư viện nữa @solana/spl-tokenbuffer-layout.

Chi tiết source code bạn xem tệp spl-token/spl-token-balance.js tại github Solana NodeJs Examples. Tôi đã cố găng làm code đơn giản để cho mọi người dễ hiểu.

Để kiểm tra số dư 1 số token trong ví, bạn gọi lệnh sau:

// Xem các lệnh hỗ trợ
node sol/solana-balance.js --help

// Xem số dư các token trong một ví trên mainnet
node spl-token/spl-token-balance.js --accountAddr=4Wiid5JonjxyH5ZPo4H2iJLXDrwESNZiXDKsp75bDkYV

// Xem số dư các token trong một ví trên testnet
node spl-token/spl-token-balance.js --nodeType=testnet --accountAddr=5W767fcieKYMDKpPYn7TGVDQpMPpFGMUHFMSqeap43Wa
Ví dụ chạy một số lệnh xem số dư các token trong 1 ví Solana
Ví dụ chạy một số lệnh xem số dư các token trong 1 ví Solana

Ví dụ 5: Tạo và chuyển SPL Token giữa các địa chỉ ví Solana

Để chuyển SPL Token giữa các ví Solana, ta tạo transaction và add transfer instruction vào. Các bước thực hiện transfer SPL Token:

  • Bước 1: Tìm “Associated Account” của Token và ví của người gửi. Trên Sollet, bạn add token này vào ví thì bạn sẽ thấy “Associated Account“.
  • Bước 2: Tìm “Associated Account” của Token và ví của người gửi.
  • Bước 3: Tạo transaction và add transfer instruction tạo từ hàm SplToken.Token.createTransferInstruction()
  • Bước 4: Gọi hàm web3.sendAndConfirmTransaction() để public transaction.
 Associated Account hiển thị trên Sollet
Associated Account hiển thị trên Sollet

Toàn bộ source code bạn xem tại tệp spl-token/spl-token-transfer.js.

Mã nguồn liên quan tới tạo SPL Token:

// Create new token
async function createToken(accountPk) {
    // Create token
    let connection = getConnection();
    let payer = web3.Keypair.fromSeed(new Uint8Array(Buffer.from(accountPk, "hex")));
    let mintToken = await splToken.Token.createMint(connection, payer, payer.publicKey, null, 6, splToken.TOKEN_PROGRAM_ID);
    console.log('Mint address:', mintToken.publicKey.toBase58());
}

Mã nguồn liên quan tới transfer SPL Token:

// Show token balance
async function transferToken(accountPk, mintAddress, toAddress, amount, decimal) {
    let connection = getConnection();
    let fromAccount = web3.Keypair.fromSeed(new Uint8Array(Buffer.from(accountPk, "hex")));
    let toAccount = new web3.PublicKey(toAddress);
    let mintAccount = new web3.PublicKey(mintAddress);
    let networkAmount = (amount*10**decimal).toFixed(0);

    // Init Token object
    let token = new splToken.Token(connection, mintAccount, splToken.TOKEN_PROGRAM_ID, fromAccount);

    // Create associated token accounts for my token if they don't exist yet
    let fromTokenAccount = await token.getOrCreateAssociatedAccountInfo(fromAccount.publicKey);
    let toTokenAccount = await token.getOrCreateAssociatedAccountInfo(toAccount);

    // Add token transfer instructions to transaction
    let transaction = new web3.Transaction();
    transaction.add(
        splToken.Token.createTransferInstruction(
            splToken.TOKEN_PROGRAM_ID,
            fromTokenAccount.address,
            toTokenAccount.address,
            fromAccount.publicKey,
            [],
            networkAmount
        )
    );
    
    // Sign transaction, broadcast, and confirm
    var signature = await web3.sendAndConfirmTransaction(connection, transaction, [fromAccount]);
    console.log("SIGNATURE", signature);
    console.log("SUCCESS");
}

Ghi chú: Code này chỉ chạy đúng nếu ví người nhận đã add token rùi. Bạn có thể mở rộng code để hỗ trợ transfer khi ví người nhận chưa add token.

Một số lệnh hỗ trợ:

// Liệt kê các lệnh hỗ trợ
node spl-token/spl-token-transfer.js --help

// Tạo 1 SPL Token trên Testnet
node spl-token/spl-token-transfer.js --action=create --accountPk=1b5f9912206718b36d230bb93fcee722dcae707bb44270821e39df7bdeb6c54a

// Tạo 1 SPL token trên Mainnet (Yêu cầu ví phải có SOL)
node spl-token/spl-token-transfer.js --action=create --nodeType=mainnet-beta --accountPk=1b5f9912206718b36d230bb93fcee722dcae707bb44270821e39df7bdeb6c54a

// Mint token trên Testnet
node spl-token/spl-token-transfer.js --action=mint --accountPk=1b5f9912206718b36d230bb93fcee722dcae707bb44270821e39df7bdeb6c54a --mintAddress=5gAveMVpKqD7PSvzp22iGRZzMPE386JsHGgjFHd5WKXB --toAddress=5W767fcieKYMDKpPYn7TGVDQpMPpFGMUHFMSqeap43Wa --amount=1000

// Transfer token trên Testnet
node spl-token/spl-token-transfer.js --action=transfer --accountPk=1b5f9912206718b36d230bb93fcee722dcae707bb44270821e39df7bdeb6c54a --mintAddress=5gAveMVpKqD7PSvzp22iGRZzMPE386JsHGgjFHd5WKXB --toAddress=XDC4iNqUJi6WAWXTwEFGo5z1AHzEVbMdB8mGi58jKsD --amount=1
Ví dụ chạy 1 số lệnh transfer SPL Token
Ví dụ chạy 1 số lệnh transfer SPL Token

Ví dụ 6: Lấy giá tỉ giá giữa các token và thực hiện swap tự động trên Raydium

Raydium là một AMM đầu tiên và cũng là AMM có TVL top 1 trên Solana. Nên có nhiều người có nhu cầu kiểm tra giá và tự động thực hiện swap.

Mình không tìm được các hàm contract để lấy giá giữa các token giống như trên BSC/Ethereum. Mình đã xem mã nguồn raydium-ui, và biết được Raydium tự tính toán giá giữa các token thông qua công thức AMM: x*y=k

Các bước tính toán giá như sau:

  • Bước 1: Xác định liquidity của token có trong Pool, thông tin các pool bạn xem tại: pools.ts
    Liquidity = <Lượng token trong Pool> + <Lượng token của open order trên AMM> – <Lượng token chuẩn bị chuyển ra>
    Tham khảo tệp liquidity.ts (Hàm actionTree() => Hàm này Raydium sẽ tính toán liquidity của tất cả các pool hỗ trợ)
  • Bước 2: Sử dụng công thưc x*y=k kết hợp phí giao dịch để tính giá

Xác định liquidity của token trong một Pool

Như vậy để tính được tỉ giá, ta cần biết x và y trong pool hiện tại, tức là ta cần xác định được liquidity của token có trong Pool.

Code trên Raydium khá phức tạp, tôi đã trích rút nó để tính thử Liquidity của pool RAY-USDC. Chi tiết source code bạn xem tại: raydium-swap/swap-liquidity-info.js, source này đã được đơn giản hóa đi rất nhiều để bạn dễ hiểu. Mã nguồn liên quan như sau:

async function getLiquidityInfos() {
    const liquidityPoolInfo = {};
    const publicKeys = [
        new web3.PublicKey(rayUsdcPool.ammId),
        new web3.PublicKey(rayUsdcPool.poolCoinTokenAccount),
        new web3.PublicKey(rayUsdcPool.poolPcTokenAccount),
        new web3.PublicKey(rayUsdcPool.ammOpenOrders)
    ];

    let rayBalance = new BigNumber(0);
    let usdcBalance = new BigNumber(0);
    const multipleInfos = await getMultipleAccounts(getConnection(), publicKeys, commitment);
    for (let idx=0; idx<multipleInfos.length; idx++) {
        let info = multipleInfos[idx];
        const data = Buffer.from(info.account.data);
        if (idx==0) {
            let parsed = AMM_INFO_LAYOUT_V4.decode(data)
            liquidityPoolInfo.swapFeeNumerator = getBigNumber(parsed.swapFeeNumerator).toFixed(0);
            liquidityPoolInfo.swapFeeDenominator = getBigNumber(parsed.swapFeeDenominator).toFixed(0);

            const { needTakePnlCoin, needTakePnlPc } = parsed;
            rayBalance = rayBalance.minus(getBigNumber(needTakePnlCoin));
            usdcBalance = usdcBalance.minus(getBigNumber(needTakePnlPc));
        } else if (idx==1) {
            const parsed = ACCOUNT_LAYOUT.decode(data)
            rayBalance =  rayBalance.plus(getBigNumber(parsed.amount));
        } else if (idx==2) {
            const parsed = ACCOUNT_LAYOUT.decode(data)
            usdcBalance =  usdcBalance.plus(getBigNumber(parsed.amount));
        } else if (idx==3) {
            const OPEN_ORDERS_LAYOUT = OpenOrders.getLayout(new web3.PublicKey(rayUsdcPool.serumProgramId))
            const parsed = OPEN_ORDERS_LAYOUT.decode(data);
            const { baseTokenTotal, quoteTokenTotal } = parsed;
            rayBalance = rayBalance.plus(getBigNumber(baseTokenTotal));
            usdcBalance = usdcBalance.plus(getBigNumber(quoteTokenTotal));
        }
    }
    liquidityPoolInfo.rayBalance = rayBalance.toFixed(0);
    liquidityPoolInfo.usdcBalance = usdcBalance.toFixed(0);
    return liquidityPoolInfo;
}

Bạn chạy lênh sau để xem Liquidity của pool RAY-USDC:
node raydium-swap/swap-liquidity-info.js

Xem liquidity của pool RAY-USDC trên Raydium
Xem liquidity của pool RAY-USDC trên Raydium

Tính toán tỉ giá giữa các token

Để xác định giá giữa các token từ công thức x*y=k, tôi tạo ra lớp AMMCalculator để thực hiện việc này.

Mã nguồn AMMCalculator:

const BigNumber = require('bignumber.js');

// Class AMM Calculator
// Calculate by formula: x*y=k
// Don't care about decimal
class AMMCalculator {
    constructor(swapFeeNumerator, swapFeeDenominator) {
        this.swapFeeNumerator = new BigNumber(swapFeeNumerator);
        this.swapFeeDenominator = new BigNumber(swapFeeDenominator);
    }

    updateLiquidity(token1Amount0, token2Amount0) {
        this.token1Amount0 = new BigNumber(token1Amount0);
        this.token2Amount0 = new BigNumber(token2Amount0);
    }

    // Swap token1 to token2
    // Need to calculate amountOut of token2 when you know amountIn of token1
    getSwapAmountOut(amountIn) {
        return this._getSwapAmountOut(this.token1Amount0, this.token2Amount0, amountIn);
    }

    // Swap token12 to token1
    // Need to calculate amountOut of token1 when you know amountIn of token2
    getReverseSwapAmountOut(amountIn) {
        return this._getSwapAmountOut(this.token2Amount0, this.token1Amount0, amountIn);
    }

    // Swap token1 to token2
    // Need to calculate amountIn of token1 you when know amountOut of token2
    getSwapAmountIn(amountOut) {
        return this._getSwapAmountIn(this.token1Amount0, this.token2Amount0, amountOut);
    }

    // Swap token2 to token1
    // Need to calculate amountIn of token2 you when know amountOut of token1
    getReverseSwapAmountIn(amountOut) {
        return this._getSwapAmountIn(this.token2Amount0, this.token1Amount0, amountOut);
    }

    _getSwapAmountOut(tokenAAmount0, tokenBAmount0, tokenAAmountIn) {
        let amountIn = new BigNumber(tokenAAmountIn);
        let amountInWithFee = amountIn
            .multipliedBy(this.swapFeeDenominator - this.swapFeeNumerator)
            .dividedBy(this.swapFeeDenominator);
        let denominator = tokenAAmount0.plus(amountInWithFee);
        let amountOut = tokenBAmount0.multipliedBy(amountInWithFee).dividedBy(denominator);
        return amountOut;
    }

    _getSwapAmountIn(tokenAAmount0, tokenBAmount0, tokenBAmountOut) {
        let amountOut = new BigNumber(tokenBAmountOut);
        let denominator = tokenBAmount0.minus(amountOut);
        let amountInWithFee = amountOut
            .multipliedBy(tokenAAmount0)
            .dividedBy(denominator);
        let amountIn = amountInWithFee
            .multipliedBy(this.swapFeeDenominator)
            .dividedBy(this.swapFeeDenominator.minus(this.swapFeeNumerator));
        return amountIn;
    }
};
module.exports = AMMCalculator;

File raydium-swap/swap-get-price.js sẽ đồng thời lấy thông tin liquidity và sử dụng AMMCalculator để tính toán giá. Bạn chạy bằng lệnh sau:

node raydium-swap/swap-get-price.js

Giá mua và bán của cặp RAY/USDC với giá trị ~ 1000 USDC
Giá mua và bán của cặp RAY/USDC với giá trị ~1000 USDC

Thực hiện swap tự động trên Raydium

Việc thực hiện swap trên Raydium nói riêng và trên Solana cũng khá phức tạp. Mình phải tham khảo swap.ts (Hàm swap()) mới biết cách thực hiện.

Mình cũng đã tối giản hết sức source code của phần swap và đóng gói vào tệp raydium-swap/swap-token.js, để mọi người đọc dễ hiểu hơn. Trong mã nguồn này mình fix 2 pool là RAY-USDC và SOL-USDC, với các pool khác cũng tương tự, chỉ cần thay đổi tham số của pool thôi. Để swap được thì 2 token RAY và USDC phải được add vào ví rùi nhé.

Source code liên quan tới swap:

// Swap
async function swap(accountPk, fromToken, toToken, aIn, aOutMin) {
    // Checking
    let poolInfo = null;
    if ((fromToken=="RAY" && toToken=="USDC") || (fromToken=="USDC" && toToken=="RAY")) {
        poolInfo = rayusdcPoolInfo;
    } else if ((fromToken=="SOL" && toToken=="USDC") || (fromToken=="USDC" && toToken=="SOL")) {
        poolInfo = solusdcPoolInfo;
    }
    if (!poolInfo) {
        console.log("fromToken/toToken is not matched with the pool!");
        return false;
    }

    // Init
    let connection = getConnection();
    let signers = [];
    let ownerAccount = web3.Keypair.fromSeed(new Uint8Array(Buffer.from(accountPk, "hex")));
    let from = TOKENS[fromToken];
    let to = TOKENS[toToken];
    const amountIn = new TokenAmount(aIn, from.decimals, false);
    const amountOutMin = new TokenAmount(aOutMin, to.decimals, false);

    // Get associated token account
    let fromMint = from.mintAddress;
    let toMint = to.mintAddress;
    let fromWrappedSolAccount = null;
    let toWrappedSolAccount = null;
    let transaction = new web3.Transaction();
    if (fromToken=="SOL") {
        fromMint = TOKENS.WSOL.mintAddress;
        fromWrappedSolAccount = await SwapUtils.createTokenAccountIfNotExist(
            connection,
            fromWrappedSolAccount,
            ownerAccount.publicKey,
            TOKENS.WSOL.mintAddress,
            getBigNumber(amountIn.wei) + 1e7,
            transaction,
            signers
        );
    }
    if (toToken=="SOL") {
        toMint = TOKENS.WSOL.mintAddress;
        toWrappedSolAccount = await SwapUtils.createTokenAccountIfNotExist(
            connection,
            toWrappedSolAccount,
            ownerAccount.publicKey,
            TOKENS.WSOL.mintAddress,
            1e7,
            transaction,
            signers
        );
    }

    // let fromTokenAccount = await SwapUtils.createAssociatedTokenAccountIfNotExist(null, ownerAccount.publicKey, fromMint, transaction);
    let fromTokenAccount = await SwapUtils.getAssociatedTokenAccount(ownerAccount.publicKey, fromMint);
    // let toTokenAccount = await SwapUtils.createAssociatedTokenAccountIfNotExist(null, ownerAccount.publicKey, toMint, transaction);
    let toTokenAccount = await SwapUtils.getAssociatedTokenAccount(ownerAccount.publicKey, toMint);
    transaction.add(
        SwapUtils.swapInstruction(
            new web3.PublicKey(poolInfo.programId),
            new web3.PublicKey(poolInfo.ammId),
            new web3.PublicKey(poolInfo.ammAuthority),
            new web3.PublicKey(poolInfo.ammOpenOrders),
            new web3.PublicKey(poolInfo.ammTargetOrders),
            new web3.PublicKey(poolInfo.poolCoinTokenAccount),
            new web3.PublicKey(poolInfo.poolPcTokenAccount),
            new web3.PublicKey(poolInfo.serumProgramId),
            new web3.PublicKey(poolInfo.serumMarket),
            new web3.PublicKey(poolInfo.serumBids),
            new web3.PublicKey(poolInfo.serumAsks),
            new web3.PublicKey(poolInfo.serumEventQueue),
            new web3.PublicKey(poolInfo.serumCoinVaultAccount),
            new web3.PublicKey(poolInfo.serumPcVaultAccount),
            new web3.PublicKey(poolInfo.serumVaultSigner),
            fromWrappedSolAccount??fromTokenAccount,
            toWrappedSolAccount??toTokenAccount,
            ownerAccount.publicKey,
            Math.floor(amountIn.toWei()),
            Math.floor(amountOutMin.toWei())
        )
    );

    if (fromWrappedSolAccount) {
        transaction.add(
            SwapUtils.closeAccountInstruction(fromWrappedSolAccount, ownerAccount.publicKey, ownerAccount.publicKey)
        );
    }
    if (toWrappedSolAccount) {
        transaction.add(
            SwapUtils.closeAccountInstruction(toWrappedSolAccount, ownerAccount.publicKey, ownerAccount.publicKey)
        );
    }
    // console.log("Signers", signers);
    // console.log("Transaction", transaction);

    // signers.push(ownerAccount);
    var signature = await web3.sendAndConfirmTransaction(
        connection,
        transaction,
        [ownerAccount, ...signers]
    );
    console.log("Signature", signature);         // Signature is the transaction hash
}

Một số lệnh hỗ trợ (Liên quan tới tham số accountPk, bạn phải đổi sang private key của ví bạn nhé):

// Xem các lệnh hỗ trợ
node raydium-swap/swap-token.js --help

// Hỗ trợ add token tới ví (Chỉ cần gọi 1 lần tiên)
node raydium-swap/swap-token.js --type=add-token --accountPk=1b5f9912206718b36d230bb93fcee722dcae707bb44270821e39df7bdeb6c54a --token=RAY

// Swap 0.01 RAY sang USDC
node raydium-swap/swap-token.js --type=swap --accountPk=1b5f9912206718b36d230bb93fcee722dcae707bb44270821e39df7bdeb6c54a --fromToken=RAY --toToken=USDC --amount=0.01

// Swap 0.1 USDC sang RAY
node raydium-swap/swap-token.js --type=swap --accountPk=1b5f9912206718b36d230bb93fcee722dcae707bb44270821e39df7bdeb6c54a --fromToken=USDC --toToken=RAY --amount=0.1

// Swap 1 USDC sang SOL
node raydium-swap/swap-token.js --type=swap --accountPk=1b5f9912206718b36d230bb93fcee722dcae707bb44270821e39df7bdeb6c54a --fromToken=USDC --toToken=SOL --amount=1

Ví dụ 7: Lấy giá tỉ giá giữa các token và thực hiện swap tự động trên Orca

Orca là AMM top 5 trên Solana, Orca thì chú trọng nhiều hơn tới trải nghiệm người dùng. Với Orca hỗ trợ cả token thường và các stable coin, vì thế Orca sử dụng cả công thức AMM và công thức sử dụng cho Stable Coin. Cách làm của Orca cũng tương tự Raydium là lấy liquidity các token trước rồi áp dụng công thức tính.

Orca cung cấp thư viện orca-sdk trên NodeJs, giúp các nhà phát triển dễ dàng tích hợp Orca vào các ứng dụng của mình. Chi tiết bạn xem tại: orca-sdk

Toàn bộ mã nguồn: solana-nodejs-examples

Một số lỗi liên quan và cách xử lý

Lỗi “SyntaxError: Unexpected token ‘||='” khi sử dụng thư viện mới @solana/web3.js

Sử dụng NodeJs 14 khi chạy với thư viện mới nhất của @solana/web3.js thì báo lỗi:

SyntaxError: Unexpected token '||='

Do thư viện mới của Solana không hỗ trợ NodeJs phiên bản 14 về trước, bạn phải nâng cấp NodeJs lên phiên bản 16 trở lên.

Nếu bạn không muốn nâng cấp NodeJs thì phải lùi thư viện @solana/web3.js về phiên bản cũ hơn bằng lệnh:

npm rm @solana/web3.js
@solana/web3.js@1.42.0

Bài viết này có hữu ích với bạn?

Kích vào một biểu tượng ngôi sao để đánh giá bài viết!

Xếp hạng trung bình 5 / 5. Số phiếu: 70

Bài viết chưa có đánh giá! Hãy là người đầu tiên đánh giá bài viết này.

3 Bình luận

  1. BlockChaien

    Cảm ơn bạn, bài viết của bạn rất chi tiết và dễ hiểu.
    Mong bạn viết thêm nhiều bài về Blockchain liên quan đến Web3, Smartcontract hay NFT

  2. Tài

    Bài viết quá hay, mình có một số thắc mắc. Nếu tác giả có thể giải đáp thì tuyệt vời quá.
    – Khi test code thì có thể chuyển SOL từ ví nào đó tới ví bất kỳ đều được. Thực tế một website phải thay đổi tham số gì để có thể làm nó hoạt động đúng? Ví dụ xác thực từ website nào, vì thực tế không thể chuyển bất kỳ như vậy được.
    Mong được trao đổi kiến thức với tác giả.

Trả lời

Giao diện bởi Anders Norén