Web3 JavaScript API

Что такое Solana-Web3.js?

Библиотека Solana-Web3.js призвана обеспечить полное покрытие Solana. Библиотека была создана на основе JSON RPC API Solana.

Общая терминология

TermDefinition
ProgramИсполняемый код без сохранения состояния, написанный для интерпретации инструкций. Программы способны выполнять действия на основе предоставленных инструкций.
InstructionНаименьшая единица программы, которую клиент может включить в транзакцию. В своем коде обработки инструкция может содержать один или несколько межпрограммных вызовов.
TransactionОдна или несколько инструкций, подписанных клиентом с использованием одной или нескольких пар ключей и выполняемых атомарно только с двумя возможными результатами: успех или неудача.

Полный список терминов см. в разделе Терминология Solana.

Начиная

Установка

yarn

$ yarn add @solana/web3.js

npm

$ npm install --save @solana/web3.js

Бандл

<!-- Development (un-minified) -->
<script src="https://unpkg.com/@solana/web3.js@latest/lib/index.iife.js"></script>

<!-- Production (minified) -->
<script src="https://unpkg.com/@solana/web3.js@latest/lib/index.iife.min.js"></script>

Использование

Javascript

const solanaWeb3 = require('@solana/web3.js');
console.log(solanaWeb3);

ES6

import * as solanaWeb3 from '@solana/web3.js';
console.log(solanaWeb3);

Бандл для браузера

// solanaWeb3 is provided in the global namespace by the bundle script
console.log(solanaWeb3);

Быстрый старт

Подключение к кошельку

Чтобы пользователи могли использовать ваше dApp или приложение на Solana, им необходимо получить доступ к своей паре ключей. Keypair — это закрытый ключ с соответствующим открытым ключом, используемый для подписи транзакций.

Есть два способа получить пару ключей:

  1. Создайте новую пару ключей
  2. Получите пару ключей, используя секретный ключ

Вы можете получить новую пару ключей следующим образом:

const {Keypair} = require("@solana/web3.js");

let keypair = Keypair.generate();

Это создаст новую пару ключей, которую пользователь сможет финансировать и использовать в вашем приложении.

Вы можете разрешить ввод секретного ключа с помощью текстового поля и получить пару ключей с помощью Keypair.fromSecretKey(secretKey).

const {Keypair} = require("@solana/web3.js");

let secretKey = Uint8Array.from([
  202, 171, 192, 129, 150, 189, 204, 241, 142,  71, 205,
  2,  81,  97,   2, 176,  48,  81,  45,   1,  96, 138,
  220, 132, 231, 131, 120,  77,  66,  40,  97, 172,  91,
  245,  84, 221, 157, 190,   9, 145, 176, 130,  25,  43,
  72, 107, 190, 229,  75,  88, 191, 136,   7, 167, 109,
  91, 170, 164, 186,  15, 142,  36,  12,  23
]);

let keypair = Keypair.fromSecretKey(secretKey);

Сегодня многие кошельки позволяют пользователям использовать свою пару ключей с помощью различных расширений или веб-кошельков. Общая рекомендация — использовать для подписи транзакций кошельки, а не пары ключей. Кошелек создает слой разделения между dApp и парой ключей, гарантируя, что dApp никогда не получит доступ к секретному ключу. Вы можете найти способы подключения к внешним кошелькам с помощью библиотеки wallet-adapter.

Создание и отправка транзакций

Чтобы взаимодействовать с программами на Solana, вы создаете, подписываете и отправляете транзакции в сеть. Транзакции представляют собой наборы инструкций с подписями. Порядок, в котором инструкции существуют в транзакции, определяет порядок их выполнения.

Транзакция в Solana-Web3.js создается с использованием объекта Transaction и добавления желаемых сообщений, адресов или инструкций.

Возьмем пример транзакции перевода:

const {Keypair, Transaction, SystemProgram, LAMPORTS_PER_SOL} = require("@solana/web3.js");

let fromKeypair = Keypair.generate();
let toKeypair = Keypair.generate();
let transaction = new Transaction();

transaction.add(
  SystemProgram.transfer({
    fromPubkey: fromKeypair.publicKey,
    toPubkey: toKeypair.publicKey,
    lamports: LAMPORTS_PER_SOL
  })
);

Приведенный выше код позволяет создать транзакцию, готовую к подписанию и передаче в сеть. В транзакцию была добавлена инструкция SystemProgram.transfer, содержащая количество отправляемых ламппортов, а также открытые ключи «куда» и «от».

Остается только подписать транзакцию парой ключей и отправить ее по сети. Вы можете выполнить отправку транзакции, используя sendAndConfirmTransaction, если вы хотите предупредить пользователя или сделать что-то после завершения транзакции, или использовать sendTransaction, если вам не нужно ждать подтверждения транзакции.

const {sendAndConfirmTransaction, clusterApiUrl, Connection} = require("@solana/web3.js");

let keypair = Keypair.generate();
let connection = new Connection(clusterApiUrl('testnet'));

sendAndConfirmTransaction(
  connection,
  transaction,
  [keypair]
);

Приведенный выше код принимает TransactionInstruction с помощью SystemProgram, создает Transaction и отправляет ее по сети. Вы используете «Соединение», чтобы определить сеть Solana, к которой вы подключаетесь, а именно «mainnet-beta», «testnet» или «devnet».

Взаимодействие с пользовательскими программами

В предыдущем разделе рассматривается отправка основных транзакций. В Solana все, что вы делаете, взаимодействует с различными программами, включая транзакцию перевода из предыдущего раздела. На момент написания программы на Solana написаны либо на Rust, либо на C.

Давайте посмотрим на SystemProgram. Сигнатура метода для выделения места в вашем аккаунте на Solana в Rust выглядит так:

pub fn allocate(
    pubkey: &Pubkey,
    space: u64
) -> Instruction

В Solana, когда вы хотите взаимодействовать с программой, вы должны сначала знать все учетные записи, с которыми вы будете взаимодействовать.

Вы должны всегда предоставлять каждую учетную запись, с которой программа будет взаимодействовать в инструкции. Кроме того, вы должны указать, является ли учетная запись isSigner или isWritable.

В приведенном выше методе «выделения» требуется один «публичный ключ» учетной записи, а также объем «пространства» для выделения. Мы знаем, что метод allocate записывает в учетную запись, выделяя пространство внутри нее, делая pubkey обязательным для записи isWritable. isSigner требуется, когда вы назначаете учетную запись, которая выполняет инструкцию. В этом случае подписывающая сторона — это учетная запись, призывающая выделить пространство внутри себя.

Давайте посмотрим, как вызвать эту инструкцию с помощью solana-web3.js:

let keypair = web3.Keypair.generate();
let payer = web3.Keypair.generate();
let connection = new web3.Connection(web3.clusterApiUrl('testnet'));

let airdropSignature = await connection.requestAirdrop(
  payer.publicKey,
  web3.LAMPORTS_PER_SOL,
);

await connection.confirmTransaction(airdropSignature);

Во-первых, мы настраиваем пару ключей учетной записи и соединение, чтобы у нас была учетная запись для выделения в тестовой сети. Мы также создаем пару ключей плательщика и раздаем несколько солей, чтобы мы могли оплатить транзакцию распределения.

let allocateTransaction = new web3.Transaction({
    feePayer: payer.publicKey
  });
let keys = [{pubkey: keypair.publicKey, isSigner: true, isWritable: true}];
let params = { space: 100 };

Мы создаем транзакцию allocateTransaction, объекты ключей и параметров. feePayer — это необязательное поле при создании транзакции, в котором указывается, кто платит за транзакцию, по умолчанию это публичный ключ первого лица, подписавшего транзакцию. keys представляет все учетные записи, с которыми будет взаимодействовать функция allocate программы. Так как функция allocate также требует места, мы создали params, которые будут использоваться позже при вызове функции allocate.

let allocateStruct = {
  index: 8,
  layout: struct([
    u32('instruction'),
    ns64('space'),
  ])
};

Вышеприведенное создано с использованием u32 и ns64 из @solana/buffer-layout для облегчения создания полезной нагрузки. Функция «allocate» принимает параметр «пробел». Чтобы взаимодействовать с функцией, мы должны предоставить данные в формате буфера. Библиотека buffer-layout помогает выделить буфер и правильно закодировать его для интерпретации программами Rust на Solana.

Давайте разберем эту структуру.

{
  index: 8, /* <-- */
  layout: struct([
    u32('instruction'),
    ns64('space'),
  ])
}

index установлен на 8, потому что функция allocate находится на 8-й позиции в перечислении инструкций для SystemProgram.

/* https://github.com/solana-labs/solana/blob/21bc43ed58c63c827ba4db30426965ef3e807180/sdk/program/src/system_instruction.rs#L142-L305 */
pub enum SystemInstruction {
    /** 0 **/CreateAccount {/**/},
    /** 1 **/Assign {/**/},
    /** 2 **/Transfer {/**/},
    /** 3 **/CreateAccountWithSeed {/**/},
    /** 4 **/AdvanceNonceAccount,
    /** 5 **/WithdrawNonceAccount(u64),
    /** 6 **/InitializeNonceAccount(Pubkey),
    /** 7 **/AuthorizeNonceAccount(Pubkey),
    /** 8 **/Allocate {/**/},
    /** 9 **/AllocateWithSeed {/**/},
    /** 10 **/AssignWithSeed {/**/},
    /** 11 **/TransferWithSeed {/**/},
}

Далее идет u32('instruction').

{
  index: 8,
  layout: struct([
    u32('instruction'), /* <-- */
    ns64('space'),
  ])
}

layout в структуре распределения всегда должен иметь u32('instruction') первым, когда вы используете его для вызова инструкции.

{
  index: 8,
  layout: struct([
    u32('instruction'),
    ns64('space'), /* <-- */
  ])
}

ns64('space') - это аргумент для функции allocate. Вы можете видеть, что в оригинальной функции allocate в Rust пространство имеет тип u64. u64 - это 64-битное целое число без знака. Javascript по умолчанию предоставляет только до 53-битных целых чисел. ns64 происходит от @solana/buffer-layout, чтобы помочь с преобразованием типов между Rust и Javascript. Вы можете найти больше преобразований типов между Rust и Javascript по адресу solana-labs/buffer-layout.

let data = Buffer.alloc(allocateStruct.layout.span);
let layoutFields = Object.assign({instruction: allocateStruct.index}, params);
allocateStruct.layout.encode(layoutFields, data);

С помощью созданного ранее bufferLayout мы можем выделить буфер данных. Затем мы назначаем нашим параметрам {space: 100}, чтобы они правильно отображались в макете, и кодируем их в буфер данных. Теперь данные готовы для отправки в программу.

allocateTransaction.add(new web3.TransactionInstruction({
  keys,
  programId: web3.SystemProgram.programId,
  data,
}));

await web3.sendAndConfirmTransaction(connection, allocateTransaction, [payer, keypair]);

Наконец, мы добавляем инструкцию транзакции со всеми ключами учетной записи, плательщиком, данными и идентификатором программы и транслируем транзакцию в сеть.

Полный код можно найти ниже.

const {struct, u32, ns64} = require("@solana/buffer-layout");
const {Buffer} = require('buffer');
const web3 = require("@solana/web3.js");

let keypair = web3.Keypair.generate();
let payer = web3.Keypair.generate();

let connection = new web3.Connection(web3.clusterApiUrl('testnet'));

let airdropSignature = await connection.requestAirdrop(
  payer.publicKey,
  web3.LAMPORTS_PER_SOL,
);

await connection.confirmTransaction(airdropSignature);

let allocateTransaction = new web3.Transaction({
  feePayer: payer.publicKey
});
let keys = [{pubkey: keypair.publicKey, isSigner: true, isWritable: true}];
let params = { space: 100 };

let allocateStruct = {
  index: 8,
  layout: struct([
    u32('instruction'),
    ns64('space'),
  ])
};

let data = Buffer.alloc(allocateStruct.layout.span);
let layoutFields = Object.assign({instruction: allocateStruct.index}, params);
allocateStruct.layout.encode(layoutFields, data);

allocateTransaction.add(new web3.TransactionInstruction({
  keys,
  programId: web3.SystemProgram.programId,
  data,
}));

await web3.sendAndConfirmTransaction(connection, allocateTransaction, [payer, keypair]);