10. Отправка новых твитов из внешнего интерфейса
Перевод | Автор оригинала: Loris
ЭПИЗОД 10
2 МЕСЯЦА НАЗАД
ЧТЕНИЕ 8 МИН.
Мы так близки к тому, чтобы иметь готовое децентрализованное приложение (dApp), которым мы можем поделиться со всем миром! Все готово, за исключением того, что мы не можем отправлять твиты из нашего интерфейса. Не очень удобно для приложения, похожего на Twitter.
Итак, давайте реализуем это прямо здесь и сейчас и завершим наше децентрализованное приложение!
Отправка твитов
После последнего эпизода вы, возможно, пытались подключить свой кошелек и отправить твит, чтобы посмотреть, что он делает. Ну, это не то, что он делает. Наша конечная точка API sendTweet не только возвращает фиктивные данные, но эти фиктивные данные теперь будут вызывать ошибку при попытке отобразить твит, поскольку мы обновили компонент TweetCard.vue в предыдущем эпизоде.
Итак, давайте начнем с удаления последнего бита фиктивных данных из нашего внешнего интерфейса и реализуем реальную логику для отправки твита в нашу программу.
Откройте файл send-tweet.js
в папке api
и замените все его содержимое следующим кодом.
import { web3 } from '@project-serum/anchor'
import { Tweet } from '@/models'
// 1. Define the sendTweet endpoint.
export const sendTweet = async ({ wallet, program }, topic, content) => {
// 2. Generate a new Keypair for our new tweet account.
const tweet = web3.Keypair.generate()
// 3. Send a "SendTweet" instruction with the right data and the right accounts.
await program.value.rpc.sendTweet(topic, content, {
accounts: {
author: wallet.value.publicKey,
tweet: tweet.publicKey,
systemProgram: web3.SystemProgram.programId,
},
signers: [tweet]
})
// 4. Fetch the newly created account from the blockchain.
const tweetAccount = await program.value.account.tweet.fetch(tweet.publicKey)
// 5. Wrap the fetched account in a Tweet model so our frontend can display it.
return new Tweet(tweet.publicKey, tweetAccount)
}
Хорошо, нам нужно кое-что объяснить.
- Прежде всего, мы определили сигнатуру нашего метода sendTweet. Как и в случае с другими конечными точками API, метод sendTweet принимает всю рабочую область в качестве первого параметра. В этом случае нам понадобится доступ к программе, а также к подключенному кошельку, который мы деструктурируем из этого первого параметра. Нам также нужен доступ к теме и содержанию твита, которые нам нужны в качестве следующих двух параметров.
- Затем мы создаем новую пару ключей для нашей новой учетной записи «Tweet». Твит будет инициализирован по общедоступному адресу этой пары ключей, и нам понадобится весь объект пары ключей, чтобы подписать транзакцию, чтобы доказать, что мы владеем этим адресом.
- Теперь у нас есть все необходимое для отправки инструкции SendTweet нашей программе Solana. Так же, как и в наших тестах, мы передаем данные, учетную запись и подписывающие лица к методу
sendTweet
API нашей программы. На этот раз мы используем подключенный кошелек в качестве учетной записи автора. - Теперь, когда инструкция отправлена, у нас должна быть учетная запись «Tweet» по предоставленному адресу «tweet.publicKey». Таким образом, мы используем его для доступа к данным нашего только что созданного твита. Мы делаем это, чтобы мы могли вернуть созданную учетную запись твита тому, кто вызывает эту конечную точку API. Это позволяет нашим компонентам автоматически добавлять его в список отображаемых твитов без повторной загрузки всех твитов на странице.
- Наконец, мы упаковываем полученные данные учетной записи и сгенерированный открытый ключ в объект
Tweet
, чтобы наш интерфейс имел все необходимое для их отображения.
Настройка формы твита
Поскольку мы внесли некоторые изменения в сигнатуру нашей конечной точки API sendTweet, давайте настроим компонент TweetForm.vue, который является единственным компонентом, который его использует.
Единственная корректировка, которую нам нужно сделать, это убедиться, что рабочая область указана в качестве первого параметра. Итак, давайте начнем с импорта useWorkspace
из компонуемых файлов.
import { computed, ref, toRefs } from 'vue'
import { useAutoresizeTextarea, useCountCharacterLimit, useSlug, useWorkspace } from '@/composables'
import { sendTweet } from '@/api'
import { useWallet } from '@solana/wallet-adapter-vue'
Далее, давайте прокрутим немного вниз и воспользуемся компонуемым useWorkspace, чтобы получить доступ к рабочему пространству и передать его методу sendTweet.
const emit = defineEmits(['added'])
const workspace = useWorkspace()
const send = async () => {
if (! canTweet.value) return
const tweet = await sendTweet(workspace, effectiveTopic.value, content.value)
emit('added', tweet)
topic.value = ''
content.value = ''
}
Обратите внимание, как мы получили доступ к объекту workspace
вне асинхронного метода send
. Это связано с тем, что компонуемый useWorkspace
опирается на API предоставления/внедрения из VueJS, который предполагается использовать при инициализации компонента. Если бы мы попытались получить доступ к рабочей области внутри метода send
, мы получили бы ошибку от VueJS при нажатии кнопки «Tweet».
Правильное обязательство
Потрясающий! Таким образом, с нашей конечной точкой API sendTweet мы должны быть в состоянии отправить наш первый твит через интерфейс, верно? К сожалению нет.
В нашем составном useWorkspace
есть небольшая ошибка, из-за которой наш код выдает следующую ошибку.
TypeError: Cannot read properties of undefined (reading 'preflightCommitment')
Ошибка в том, что мы предоставили только два параметра при создании экземпляра нашего объекта Provider
, когда мы должны были указать три.
const provider = computed(() => new Provider(connection, wallet.value)) // <- Missing 3rd parameter.
Отсутствующий третий параметр — это объект конфигурации, который используется для определения фиксации наших транзакций.
Чтобы исправить это, мы могли бы просто указать пустой массив — т. е. {}
— в качестве третьего аргумента, который бы вернулся к конфигурациям по умолчанию.
Однако я хотел бы, чтобы мы воспользовались этой возможностью, чтобы понять, какие конфигурации необходимы и как мы можем предоставить их в явном виде.
Этот объект конфигурации принимает два свойства: commitment
и preflightCommitment
. Оба они определяют уровень обязательств, который мы ожидаем от блокчейна при отправке транзакции. Единственная разница между ними заключается в том, что «preflightCommitment» будет использоваться при моделировании транзакции, тогда как «commitment» будет использоваться при реальной отправке транзакции.
Если вам интересно, зачем нам нужно имитировать транзакцию, хорошим примером может быть то, что ваш кошелек покажет вам сумму денег, которая, как ожидается, будет получена или потеряна в результате транзакции, прежде чем она будет одобрена.
Что же такое «обязательство»? Согласно документации Solana, ** обязательство описывает, насколько завершен блок в момент отправки транзакции. **. Когда мы отправляем транзакцию в блокчейн, она добавляется в блок, который необходимо «финализировать», прежде чем он официально станет частью данных блокчейна. Прежде чем блок будет «финализирован», он должен быть «подтвержден» системой голосования, созданной на кластере. Прежде чем блок будет «подтвержден», существует вероятность того, что блок будет пропущен кластером.
Таким образом, существует 3 уровня обязательств, которые точно соответствуют сценариям, описанным выше. Они в порядке убывания приверженности:
finalized
. Это означает, что мы можем быть уверены, что блок не будет пропущен и, следовательно, транзакция не будет откатана.confirmed
. Это означает, что кластер подтвердил посредством голосования, что блок транзакции действителен. Хотя это явный признак того, что транзакция не будет отменена, это все же не гарантия.processed
. Это означает, что транзакция была обработана и добавлена в блок, и нам не нужны какие-либо гарантии того, что произойдет с этим блоком.
Итак, какой уровень обязательств мы должны выбрать для нашего маленького приложения, похожего на Twitter? Глядя на документацию Solana, они рекомендуют следующее.
При запросе состояния леджера рекомендуется использовать более низкие уровни обязательств для отчета о ходе выполнения и более высокие уровни, чтобы гарантировать, что состояние не будет отменено.
В нашем случае я бы не считал откат твита критической проблемой. Кроме того, очень маловероятно, что блок, содержащий нашу транзакцию, будет пропущен кластером. Таким образом, уровень фиксации «обработано» достаточно хорош для нашего приложения. Мы будем использовать его как для смоделированных, так и для реальных транзакций.
Обратите внимание, что использование «завершенного» уровня обязательств может быть более подходящим для (не смоделированных) финансовых транзакций с критическими последствиями.
Теперь, когда мы знаем, какие уровни обязательств использовать, давайте явно настроим их в нашем компонуемом useWorkspace.js
. Во-первых, мы определили две переменные preflightCommitment
и commitment
для смоделированных и реальных транзакций соответственно.
// ...
const preflightCommitment = 'processed'
const commitment = 'processed'
const programID = new PublicKey(idl.metadata.address)
const workspaceSymbol = Symbol()
Затем мы передаем эти уровни фиксации конструктору Provider в качестве объекта конфигурации. Мы также задаем переменную commitment
в качестве второго параметра нашего объекта Connection
, чтобы он мог использовать ее в качестве резервного уровня фиксации, когда он не указан непосредственно в транзакции.
export const initWorkspace = () => {
const wallet = useAnchorWallet()
const connection = new Connection('http://127.0.0.1:8899', commitment)
const provider = computed(() => new Provider(connection, wallet.value, { preflightCommitment, commitment }))
const program = computed(() => new Program(idl, programID, provider.value))
// ...
}
Раздача SOL
Хорошо, теперь мы точно можем отправить твит через наш интерфейс?! Почти… 😅
Если мы попытаемся отправить твит прямо сейчас, то получим следующую ошибку в консоли.
Attempt to debit an account but found no record of a prior credit.
Хорошо, мы видели эту ошибку в прошлом. Это значит, что у нас нет денег. Помните, как я говорил вам, что запуск локальной книги всегда дает 500 миллионов SOL в кошелек вашей локальной машины для запуска тестов? Ну, проблема в том, что мы не используем этот кошелек в нашем браузере. Вместо этого мы используем наш настоящий кошелек в локальном кластере.
Это означает, что нам нужно явно раздать себе немного денег, прежде чем мы сможем отправлять транзакции.
Для этого нам сначала нужно узнать открытый ключ нашего реального кошелька. Для этой цели мы можем использовать выпадающее меню кнопки кошелька. После подключения кошелька нажмите кнопку кошелька на боковой панели и выберите «Копировать адрес».
Затем мы можем использовать команду solana airdrop, за которой следует количество SOL, которое мы хотим раздать, и адрес учетной записи для кредита.
Нам не нужно много денег, но давайте дадим себе 1000 SOL — почему бы и нет, мы это заслужили. Затем вставьте свой открытый ключ и запустите эту команду. Вот как это выглядит для меня.
solana airdrop 1000 B1AfN7AgpMyctfFbjmvRAvE1yziZFDb9XCwydBjJwtRN
# Outputs:
# Requesting airdrop of 1000 SOL
#
# Signature: 4rBNKMyRTcddaT9QHtYSD62Juk5F4AdgryiuE4N83Yj8JJVTomAnHWL8xvPitJdtDdLorSf81rBsYz89r7dXis6y4
#
# 1000 SOL
Хорошо, теперь мы наконец-то можем отправить наш первый твит из внешнего интерфейса!
Введите некоторый контент и, при желании, тему, прежде чем нажимать кнопку «Твитнуть». Вы должны получить всплывающее окно от поставщика кошелька с просьбой одобрить транзакцию.
В зависимости от поставщика вашего кошелька вы также должны увидеть оценку того, сколько SOL эта транзакция зачислит или спишет с вашего счета. Однако при использовании Phantom это часто не работает для меня при использовании локального кластера, поскольку симулированная транзакция просто зацикливается навсегда.
Тем не менее, нажатие на «Подтвердить» должно запустить транзакцию по-настоящему и отобразить новый твит вверху списка!
Если вы обновите страницу, вы увидите, что наш новый твит правильно сохранился в нашем локальном реестре.
Вывод
Черт возьми, наше децентрализованное приложение наконец-то заработало! 🔥🔥🔥
Теперь все, что осталось сделать, это развернуть его в реальном кластере, чтобы поделиться им с остальным миром. Именно этим мы и займемся в следующем выпуске!