Транзакция
В блокчейне TON любое изменение состояния аккаунта записывается с помощью транзакции. В отличие от сообще ний, транзакции ничего не перемещают, отправляют или получают. Эти термины часто путают, но важно понимать, что транзакция — это просто запись всех изменений, произошедших с конкретным аккаунтом.
В этом разделе вы узнаете, как устроена транзакция, как она проходит через каждую фазу, как получать данные о транзакциях с помощью API и как определить, завершилось ли событие в блокчейне успехом.
Структура транзакции
TL-B
Перед тем, как погружаться в принципы работы транзакций, нам нужно понять их структуру с помощью TL-B (Type Language – Binary). Стоит отметить, что в TON существует несколько типов транзакций; однако в этом руководстве мы сосредоточимся исключительно на обычной транзакции. Такие транзакции актуальны для обработки платежей и разработки большинства приложений на TON.
trans_ord$0000 credit_first:Bool
storage_ph:(Maybe TrStoragePhase)
credit_ph:(Maybe TrCreditPhase)
compute_ph:TrComputePhase action:(Maybe ^TrActionPhase)
aborted:Bool bounce:(Maybe TrBouncePhase)
destroyed:Bool
= TransactionDescr;
Согласно схеме TL-B, транзакция состоит из следующих полей:
| Поле | Тип | Описание |
|---|---|---|
credit_first | Bool | Указывает, должна ли фаза кредита исполняться первой. Это зависит от того, выставлен ли флаг возврата. Это будет объяснено подробнее дальше. |
storage_ph | Maybe TrStoragePhase | Фаза хранения, отвечает за обработку комиссий, связанных с постоянным хранилищем аккаунта. |
credit_ph | Maybe TrCreditPhase | Фаза кредита, отвечает за передачу средств, доставленных с входящим сообщением, если оно внутреннее (тип #1 или #2). |
compute_ph | TrComputePhase | Фаза вычислений, отвечает за исполнение кода смарт-контракта, хранящегося у аккаунта в ячейке code. |
action | Maybe ^TrActionPhase | Фаза действий, отвечает за обработку любых действий, инициированных в ходе фазе вычислений. |
aborted | Bool | Указывает, была ли транзакция прервана на одном из этапов. Если true, транзакция не была выполнена, и изменения из фаз compute_ph и action не были применены. |
bounce | Maybe TrBouncePhase | Фаза отскока, отвечающая за обработку ошибок, произошедших на этапах compute_ph или action. |
destroyed | Bool | Указывает, был ли аккаунт уничтожен во время выполнения транзакции. |
Другие типы транзакций, например, trans_storage, trans_tick_tock и trans_split_prepare, используются для внутренних событий, невидимых для конечных пользователей. К ним относятся разделение и слияние шардов, tick-tock-транзакции и т. д.
Поскольку они не имеют отношения к разработке dApp, мы не будем рассматривать их в этом руководстве.
Фаза кредита (credit phase)
Эта фаза (credit phase) относительно небольшая и простая. Если вы посмотрите исходный код блокчейна, увидите, что её основная логика заключается в пополнении баланса контракта оставшимися средствами из входящего сообщения.
credit_phase->credit = msg_balance_remaining;
if (!msg_balance_remaining.is_valid()) {
LOG(ERROR) << "cannot compute the amount to be credited in the credit phase of transaction";
return false;
}
// NB: msg_balance_remaining may be deducted from balance later during bounce phase
balance += msg_balance_remaining;
if (!balance.is_valid()) {
LOG(ERROR) << "cannot credit currency collection to account";
return false;
}
Фаза кредита сериализуется в TL-B следующим образом:
tr_phase_credit$_ due_fees_collected:(Maybe Grams)
credit:CurrencyCollection = TrCreditPhase;
Эта фаза состоит из двух полей:
| Поле | Тип | Описание |
|---|---|---|
due_fees_collected | Maybe Grams | Сумма взимаемых сборов за хранение. Это поле присутствует, если у аккаунта нет средств на балансе, и накопился долг за оплату хранилища. |
credit | CurrencyCollection | Сумма, зачисленная аккаунту в результате получения сообщения. |
Фаза хранения (storage phase)
В этой фазе блокчейн обрабатывает комиссии, связанные с постоянным хранилищем аккаунта. Для начала посмотрим на схему TL-B:
tr_phase_storage$_ storage_fees_collected:Grams
storage_fees_due:(Maybe Grams)
status_change:AccStatusChange
= TrStoragePhase;
Эта фаза включает следующие поля:
| Поле | Тип | Описание |
|---|---|---|
storage_fees_collected | Grams | Сумма комиссии за хранение данных, взимаемая с аккаунта. |
storage_fees_due | Maybe Grams | Сумма комиссии за хранение данных, которая была начислена, однако не могла быть получена из-за недостаточного баланса. Эта сумма представляет собой накопленный долг. |
status_change | AccStatusChange | Изменение статуса аккаунта после исполнения транзакции. |
Поле storage_fees_due имеет тип Maybe, потому что оно присутствует только тогда, когда у аккаунта недостаточный баланс для покрытия комиссии за хранение. Когда у аккаунта достаточно средств, это поле опускается.
Поле AccStatusChange указывает, из менился ли статус аккаунта на этом этапе. Например:
- Если долг превышает 0,1 TON, аккаунт переходит в статус frozen.
- Если долг превышает 1 TON, аккаунт удаляется.
Фаза вычислений (compute phase)
Эта фаза — одна из самых сложных в транзакции. Здесь выполняется код смарт-контракта, сохранённый в состоянии аккаунта.
В отличие от предыдущих фаз, определение TL-B для этой включает различные варианты.
tr_phase_compute_skipped$0 reason:ComputeSkipReason
= TrComputePhase;
tr_phase_compute_vm$1 success:Bool msg_state_used:Bool
account_activated:Bool gas_fees:Grams
^[ gas_used:(VarUInteger 7)
gas_limit:(VarUInteger 7) gas_credit:(Maybe (VarUInteger 3))
mode:int8 exit_code:int32 exit_arg:(Maybe int32)
vm_steps:uint32
vm_init_state_hash:bits256 vm_final_state_hash:bits256 ]
= TrComputePhase;
cskip_no_state$00 = ComputeSkipReason;
cskip_bad_state$01 = ComputeSkipReason;
cskip_no_gas$10 = ComputeSkipReason;
cskip_suspended$110 = ComputeSkipReason;
Для начала обратите внимание, что фаза вычислений может быть полностью пропущена. В этом случае причина для пропуска указывается явным образом, и может быть одной из следующих:
| Причина пропуска | Описание |
|---|---|
cskip_no_state | У смарт-контракта нет состояния, а следовательно, и кода, поэтому его исполнение невозможно. |
cskip_bad_state | Возникает в двух случаях: когда поле fixed_prefix_length имеет недопустимое значение или когда StateInit, предоставленный во входящем сообщении, не соответствует адресу аккаунта. |
cskip_no_gas | Входящее сообщение не предоставило достаточно TON для покрытия газа, необходимого для исполнения смарт-контракта. |
cskip_suspended | Аккаунт заморожен, так что исполнение его кода недоступно. Этот вариант был использован для заморозки аккаунтов ранних майнеров в ходе стабилизации токеномики TON. |
Поле fixed_prefix_length можно использовать для указания фиксированного префикса для адреса аккаунта, чтобы быть уверенным, что аккаунт находится в определённом шарде. Эта тема выходит за рамки данного руководства, но дополнительную информацию можно найти [здесь](https://github.com/ton-blockchain/ton/blob/master/doc/GlobalVersions.md#anycast- addresses-and-address-rewrite).
Теперь, когда мы разобрали причины, по которым фаза вычислений может быть пропущена, давайте разберёмся с ситуацией, когда код смарт-контракта всё же выполняется. Для описания результата используются следующие поля:
| Поле | Тип | Описание |
|---|---|---|
success | Bool | Показывает, завершилась ли фаза вычислений успешно. Если значение false, то любые изменения состояния, сделанные в ходе этой фазы, признаются недействительными. |
msg_state_used, account_activated, mode, vm_init_state_hash, vm_final_state_hash | - | Эти поля сейчас не используются в блокчейне. Их значения всегда записываются как нули. |
gas_fees | Grams | Сумма комиссий, уплаченных за исполнение кода смарт-контракта. |
gas_used, gas_limit | VarUInteger | Фактическое количество использованного газа и лимит, установленный на его расход в ходе выполнения контракта. |
gas_credit | Maybe (VarUInteger 3) | Используется только во внешних сообщениях. Поскольку они не могут содержать TON, выдаётся маленький кредит газа, чтобы смарт-контракт мог начать исполнение и определить, хочет ли он продолжать использовать свой баланс. |
exit_code | int32 | Код возврата виртуальной машины. Значение 0 или 1 (альтернативный успех) означает успешное выполнение. Любое другое означает, что код контракта завершил выполнение с ошибкой — за исключением случаев, где использовалась инструкция commit. Примечание: для удобства разработчики часто называют это кодом возврата смарт-контракта, хотя это технически неточно. |
exit_arg | Maybe int32 | Виртуальная машина может выбрасывать необязательный аргумент в случае сбоя. Полезно для отладки ошибок смарт-контрактов. |
vm_steps | uint32 | Количество шагов, выполненных виртуальной машиной во время выполнения кода. |
Инструкция commit используется для сохранения любых изменений, сделанных до её вызова, даже если позже в ходе той же фазы произойдёт ошибка. Эти изменения будут отменены только в случае сбоя фазы действий.
Фаза действий (action phase)
Когда исполнение кода смарт-контракта завершается, начинается фаза действий. Если в ходе фазы вычислений были созданы любые действия, то они обрабатываются на этой стадии.
В TON есть ровно 4 типа возможных действий:
action_send_msg#0ec3c86d mode:(## 8)
out_msg:^(MessageRelaxed Any) = OutAction;
action_set_code#ad4de08e new_code:^Cell = OutAction;
action_reserve_currency#36e6b809 mode:(## 8)
currency:CurrencyCollection = OutAction;
libref_hash$0 lib_hash:bits256 = LibRef;
libref_ref$1 library:^Cell = LibRef;
action_change_library#26fa1dd4 mode:(## 7)
libref:LibRef = OutAction;
| Тип | Описание |
|---|---|
action_send_msg | Отправляет сообщение. |
action_set_code | Обновляет код смарт-контракта. |
action_reserve_currency | Резервирует часть баланса аккаунта. Это особенно полезно для управления газом. |
action_change_library | Меняет библиотеку, используемую смарт-контрактом. |
Эти действия выполняются в порядке их создания во время исполнения кода. Всего может быть создано до 255 действий.
Теперь давайте изучим схему TL-B, определяющую фазу действий.
tr_phase_action$_ success:Bool valid:Bool no_funds:Bool
status_change:AccStatusChange
total_fwd_fees:(Maybe Grams) total_action_fees:(Maybe Grams)
result_code:int32 result_arg:(Maybe int32) tot_actions:uint16
spec_actions:uint16 skipped_actions:uint16 msgs_created:uint16
action_list_hash:bits256 tot_msg_size:StorageUsed
= TrActionPhase;
Она включает следующие поля:
| Поле | Тип | Описание |
|---|---|---|
success | Bool | Указывает, была ли успешно завершена фаза действий. При значении false все изменения, сделанные на этом этапе, отбрасываются. Изменения, сделанные на этапе вычислений, также отменяются. |
valid | Bool | Указывает, была ли фаза действий валидной. Значение false означает, что во время выполнения смарт-контракта были созданы невалидные действия. Каждый тип действия имеет свои критерии валидности. |
no_funds | Bool | Указывает, было ли на счету достаточно средств для выполнения действий. Если значение false, фаза действий была прервана из-за нехватки средств. |
status_change | AccStatusChange | Изменение статуса аккаунта после фазы действий. Поскольку удаление аккаунта происходит через действия (через режим 32), это поле может указывать, был ли аккаунт удалён. |
total_fwd_fees | Maybe Grams | Общая сумма комиссий за пересылку, уплаченных за сообщения, созданные на этапе действий. |
total_action_fees | Maybe Grams | Общая сумма комиссий, уплаченных за выполнение действий. |
result_code | int32 | Код результата выполнения действий. Значение 0 означает, что все действия были успешно завершены. |
result_arg | Maybe int32 | Сообщение об ошибке, возвращаемое в случае ошибки. Полезно для отладки кода смарт-контракта. |
tot_actions | uint16 | Общее количество действий, созданных во время выполнения смарт-контракта. |
spec_actions | uint16 | Количество специальных действий (все, кроме action_send_msg). |
skipped_actions | uint16 | Количество действий, пропущенных во время выполнения смарт-контракта. Относится к случаям, когда отправка сообщений завершилась неудачей, но был установлен флаг ignore_errors (значение 2). |
msgs_created | uint16 | Количество сообщений, созданных во время выполнения действия. |
action_list_hash | bits256 | Хеш списка действий. |
tot_msg_size | StorageUsed | Суммарный размер всех сообщений. |
Фаза возврата (bounce phase)
Если фаза вычислений или фаза действий завершаются ошибкой, и у входящего сообщения установлен флаг bounce, система вызывает фазу возврата (bounce phase, также можно перевести как «фаза отскока»).
Чтобы фаза возврата сработала из-за ошибки в фазе действий, у неудачного действия должен быть установлен флаг 16, который позволяет возврат при ошибке.
tr_phase_bounce_negfunds$00 = TrBouncePhase;
tr_phase_bounce_nofunds$01 msg_size:StorageUsed
req_fwd_fees:Grams = TrBouncePhase;
tr_phase_bounce_ok$1 msg_size:StorageUsed
msg_fees:Grams fwd_fees:Grams = TrBouncePhase;
Тип tr_phase_bounce_negfunds не используется в текущей версии блокчейна. Два других типа функционируют следующим образом:
| Тип | Описание |
|---|---|
tr_phase_bounce_nofunds | Указывает, что у аккаунта недостаточно средств для обработки сообщения, которое должно быть возвращено отправителю. |
tr_phase_bounce_ok | Указывает, что система успешно обрабатывает возврат и отправляет сообщение обратно отправителю. |
На этом этапе msg_fees и fwd_fees рассчитываются на основе общей комиссии за пересылку fwd_fees для сообщения:
- Одна треть комиссии идет в
msg_feesи взимается немедленно. - Оставшиеся две трети идут в
fwd_fees.
Полное тело транзакции
Теперь, когда мы рассмотрели заголовок транзакции и её описание, мы можем посмотреть, как выглядит полная транзакция в TON. Сначала изучим схему TL-B:
transaction$0111 account_addr:bits256 lt:uint64
prev_trans_hash:bits256 prev_trans_lt:uint64 now:uint32
outmsg_cnt:uint15
orig_status:AccountStatus end_status:AccountStatus
^[ in_msg:(Maybe ^(Message Any)) out_msgs:(HashmapE 15 ^(Message Any)) ]
total_fees:CurrencyCollection state_update:^(HASH_UPDATE Account)
description:^TransactionDescr = Transaction;
Она показывает, что транзакция включает следующие поля:
| Поле | Тип | Описание |
|---|---|---|
account_addr | bits256 | Адрес аккаунта, к которому относится транзакция. |
lt | uint64 | Параметр logical time транзакции. |
prev_trans_hash | bits256 | Хеш предыдущей транзакции, выполненной на этом аккаунте. |
prev_trans_lt | uint64 | Параметр logical time предыдущей транзакции этого аккаунта. |
now | uint32 | Временная метка Unix с временем создания транзакции. |
outmsg_cnt | uint15 | Количество исходящих сообщений, сгенерированных во время выполнения транзакции. |
orig_status | AccountStatus | Статус аккаунта до транзакции. |
end_status | AccountStatus | Статус аккаунта после транзакции. |
in_msg | Maybe ^(Message Any) | Входящее сообщение, обработанное во время транзакции. У обычных транзакций это поле всегда присутствует. |
out_msgs | HashmapE 15 ^(Message Any) | Исходящие сообщения, сгенерированные во время транзакции. |
total_fees | CurrencyCollection | Общая сумма комиссий, уплаченных за выполнение транзакции. |
state_update | ^(HASH_UPDATE Account) | Содержит хеши предыдущего и нового состояний аккаунта. |
description | ^TransactionDescr | Описание транзакции, содержащее детали фазы выполнения. Мы рассмотрели это ранее. |
Поля orig_status и end_status указывают, как изменяется состояние аккаунта в результате транзакции. Существует 4 возможных статуса:
acc_state_uninit$00 = AccountStatus;
acc_state_frozen$01 = AccountStatus;
acc_state_active$10 = AccountStatus;
acc_state_nonexist$11 = AccountStatus;
Как получить доступ к данным о транзакциях
Как получить транзакцию с помощью api/v2
Среди поддерживаемых API с открытым исходным кодом мы можем использовать TON Center APIv2 и APIv3. APIv2 — это более «сырая» версия, предоставляющая только базовый доступ к данным блокчейна. Для получения транзакции есть два варианта:
- Использовать эндпоинт
/api/v2/getTransactions:
- JavaScript
import axios from 'axios';
async function main() {
const client = axios.create({
baseURL: 'https://toncenter.com/api/v2',
timeout: 5000,
headers: {
'X-Api-Key': 'put your api key', // you can get an api key from @tonapibot bot in Telegram
},
});
const address = 'UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA';
const response = await client.get('/getTransactions', {
params: {
address: address,
limit: 1,
to_lt: 0,
archival: false,
},
headers: {
'X-Api-Key': 'put your api key', // you can get an api key from @tonapibot bot in Telegram
},
});
console.log(response.data);
}
main().finally(() => console.log('Exiting...'));
- Использовать протокол
JSON-RPC:
- JavaScript
import { Address, TonClient } from '@ton/ton';
async function main() {
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
apiKey: 'put your api key', // you can get an api key from @tonapibot bot in Telegram
});
const address = Address.parse('UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA');
const response = await client.getTransactions(address, {
limit: 1,
});
console.log(response[0]);
}
main().finally(() => console.log('Exiting...'));
Рекомендуемый подход — использовать JSON-RPC, поскольку он интегрируется с существующими SDK, где все поля предопределены и правильно типизированы. Это избавляет от необходимости интерпретировать каждое поле вручную.
When retrieving transactions, you might encounter the following error:
LITE_SERVER_UNKNOWN: cannot compute block with specified transaction: cannot find block (0,ca6e321c7cce9ece) lt=57674065000003: lt not in db.
Это означает, что транзакции аккаунта старые, и блоки, содержащие их, больше не хранятся на LiteServer. В этом случае вы можете использовать опцию archival: true для получения данных с архивного узла.
Как получить транзакцию с помощью api/v3
Вариант APIv3 более продвинут и удобен для получения различных событий из блокчейна. Например, он позволяет получать информацию о переводах NFT, операциях с токенами и даже транзакциях в статусе pending. В этом руководстве мы сосредоточимся только на эндпоинте transactions, который возвращает завершенные транзакции:
APIv3 более продвинут и удобен для доступа к различным типам событий блокчейна. Например, он позволяет получать данные о переводах NFT, перемещениях токенов и даже транзакциях, которые все ещё находятся в состоянии ожидания обработки (pending).
В этом руководстве мы сосредоточимся только на эндпоинте transactions, который возвращает подтвержденные транзакции.
- JavaScript
import axios from 'axios';
async function main() {
const client = axios.create({
baseURL: 'https://toncenter.com/api/v3',
timeout: 5000,
headers: {
'X-Api-Key': 'put your api key', // you can get an api key from @tonapibot bot in Telegram
},
});
const address = 'UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA';
const response = await client.get('/transactions', {
params: {
account: address,
limit: 1,
to_lt: 0,
archival: false,
},
});
console.log(response.data.transactions[0]);
}
main().finally(() => console.log('Exiting...'));
Если вы изучите ответ, вы увидите, что он значительно отличается от вывода APIv2. Ключевое отличие заключается в том, что APIv3 индексирует транзакции, в то время как предыдущая версия действует только как обёртка вокруг LiteServer. В API v3 вся информация поступает непосредственно из базы данных сервера.
Это позволяет API возвращать предварительно обработанные данные. Например, при изучении полей account_state_before и account_state_after вы обнаружите, что они включают не только хеш состояния аккаунта, но и полные данные, такие как код, данные, баланс TON и даже баланс ExtraCurrency.
[
account_state_before: {
hash: 'Rljfqi3l3198Fok7x1lyf9OlT5jcVRae7muNhaOyqNQ=',
balance: '235884286762',
extra_currencies: {},
account_status: 'active',
frozen_hash: null,
data_hash: 'uUe+xBA4prK3EyIJ8iBk8unWktT4Grj+abz4LF2opX0=',
code_hash: '/rX/aCDi/w2Ug+fg1iyBfYRniftK5YDIeIZtlZ2r1cA='
},
account_state_after: {
hash: 'asmytWJakUpuVVYtuSMgwjmlZefj5tV5AgnWgGYP+Qo=',
balance: '225825734714',
extra_currencies: {},
account_status: 'active',
frozen_hash: null,
data_hash: '6L0wUi1S55GRvdizozJj2GkCqjKSx8iK7dEHlTOe8d0=',
code_hash: '/rX/aCDi/w2Ug+fg1iyBfYRniftK5YDIeIZtlZ2r1cA='
}
]
Кроме того, ответ включает поле address_book из массива адресов, с которыми аккаунт взаимодействовал во время выполнения транзакции.
Поля транзакций в SDK
При проверке ответа, возвращаемого по протоколу JSON-RPC в @ton/ton@, вы можете заметить два дополнительных поля — hash и raw — которые не являются частью данных транзакции в блокчейне. SDK добавляет эти поля для удобства.
- Поле
hashпредоставляет функцию, позволяющую вычислить хеш транзакции. - Поле
rawсодержит BoC транзакции, который вы можете разобрать самостоятельно, используя либо встроенный метод из SDK, либо вручную.
- JavaScript
import { Address, loadTransaction, TonClient } from '@ton/ton';
async function main() {
const client = new TonClient({
endpoint: "https://toncenter.com/api/v2/jsonRPC",
apiKey: "put your api key", // you can get an api key from @tonapibot bot in Telegram
});
const address = Address.parse('UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA');
const response = await client.getTransactions(address, {
limit: 1,
});
const transaction = response[0];
console.log(loadTransaction(transaction.raw.beginParse()));
console.log(`Transaction hash: ${transaction.hash().toString('hex')}`);
}
main().finally(() => console.log("Exiting..."));
Какой API использовать?
После рассмотрения API v2 и API v3 напрашивается вопрос, какой из них выбрать, и ответ полностью зависит от вашего конкретного сценария использования.
В качестве общей рекомендации вы можете использовать протокол JSON-RPC из APIv2, поскольку он позволяет вам полагаться на существующий SDK, который уже предоставляет все необходимые методы и типы.
Если эта функциональность не покрывает все ваши требования, вам следует рассмотреть полный или частичный переход на API v3 или изучить другие API в экосистеме, которые могут предлагать больше данных.