Это руководство содержит пошаговые инструкции о том, как
- написать смарт-контракт в Sway;
- написать тест в Rust;
- развернуть в тестовой сети Fuel;
- создать интерфейс;
- интегрировать кошелек.
========================
Чтобы установить набор инструментов Fuel, вы можете использовать скрипт fuelup-init. Это установит forc, forc-client, forc-fmt, forc-lsp, forc-wallet, а также fuel-core в ~/.fuelup/bin.
curl https://install.fuel.network | sh
Проверить версии необходимых инструментов вы можете с помощью приведенных ниже команд:
fuelup self update
fuelup update
fuelup default latest
========================
Мы создадим простой контракт счетчика с двумя функциями: одна для увеличения счетчика и одна для возврата значения счетчика. Для начала создадим новую пустую папку под названием fuel-project:
mkdir fuel-project
Переместимся в папку проекта и создадим проект контракта, используя forc:
cd fuel-project
forc new counter-contract
Откроем свой проект в редакторе кода ./counter-contract/src/main.sw, удалим все в src/main.sw и создадим новый код:
contract;
storage {
counter: u64 = 0,
}
abi Counter {
#[storage(read, write)]
fn increment();
#[storage(read)]
fn count() -> u64;
}
impl Counter for Contract {
#[storage(read)]
fn count() -> u64 {
storage.counter.read()
}
#[storage(read, write)]
fn increment() {
let incremented = storage.counter.read() + 1;
storage.counter.write(incremented);
}
}
Перейдем в папку нашего контракта и выполним команду, чтобы создать контракт:
cd counter-contract
forc build
========================
Если у вас еще не установлен Rust, вы можете установить его, выполнив следующую команду:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Далее установим cargo generate:
cargo install cargo-generate --locked
Теперь cгенерируем тестовую программу по умолчанию с помощью следующей команды:
cargo generate --init fuellabs/sway templates/sway-test-rs --name counter-contract
Откройте файл Cargo.toml и проверьте версию fuels, используемого в зависимости от разработки. Измените версию на 0.62.0, если это еще не так.
Вот так должен выглядеть ваш файл ./counter-contract/tests/harness.rs
use fuels::{prelude::*, types::ContractId};
// Load abi from json
abigen!(Contract(
name = "MyContract",
abi = "out/debug/counter-contract-abi.json"
));
async fn get_contract_instance() -> (MyContract<WalletUnlocked>, ContractId) {
// Launch a local network and deploy the contract
let mut wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new(
Some(1), /* Single wallet */
Some(1), /* Single coin (UTXO) */
Some(1_000_000_000), /* Amount per coin */
),
None,
None,
)
.await
.unwrap();
let wallet = wallets.pop().unwrap();
let id = Contract::load_from(
"./out/debug/counter-contract.bin",
LoadConfiguration::default(),
)
.unwrap()
.deploy(&wallet, TxPolicies::default())
.await
.unwrap();
let instance = MyContract::new(id.clone(), wallet);
(instance, id.into())
}
#[tokio::test]
async fn can_get_contract_id() {
let (_instance, _id) = get_contract_instance().await;
// Now you have an instance of your contract you can use to test each function
}
#[tokio::test]
async fn test_increment() {
let (instance, _id) = get_contract_instance().await;
// Increment the counter
instance.methods().increment().call().await.unwrap();
// Get the current value of the counter
let result = instance.methods().count().call().await.unwrap();
// Check that the current value of the counter is 1.
// Recall that the initial value of the counter was 0.
assert_eq!(result.value, 1);
}
Запускаем cargo test в терминале:
cargo test
Если все пойдет хорошо, результат должен выглядеть следующим образом:
...
running 2 tests
test can_get_contract_id ... ok
test test_increment ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.25s
Пришло время развертывания смарт-контракта, сделаем это с помощью forc из командной строки. Чтобы развернуть контракт, вам необходимо иметь кошелек для подписи транзакции и монеты для оплаты газа. Если у вас уже есть fuel кошелек, то импортируйте его командой:
forc wallet import
Если нет, то создайте новый кошелек (не забудьте записать пароль и мнемоническую фразу от кошелька):
forc wallet new
Затем создаем новую учетную запись кошелька с помощью:
forc wallet account new
Если вам нужно составить список своих учетных записей, вы можете запустить команду ниже:
forc wallet accounts
Вы можете получить тестовые средства с помощью крана https://faucet-testnet.fuel.network/
Теперь вы можете развернуть контракт в последней тестовой сети с помощью команды:
forc deploy --testnet
Укажите пароль вашего кошелька, затем введите номер предпочтительного счета и нажмите Y, когда будет предложено принять транзакцию. Готово!
Сохраните Contract ID, так как он понадобится вам позже для подключения внешнего интерфейса.
Contract counter-contract Deployed!
Network: https://testnet.fuel.network
Contract ID: 0x8342d413de2a678245d9ee39f020795800c7e6a4ac5ff7daae275f533dc05e08
Deployed in block 0x4ea52b6652836c499e44b7e42f7c22d1ed1f03cf90a1d94cd0113b9023dfa636
========================
Для начала установим fuel wallet https://chromewebstore.google.com/detail/fuel-wallet/dldjpboieedgcmpkchcjcbijingjcgok
После настройки кошелька нажмите кнопку «Кран» в кошельке, чтобы получить токены тестовой сети.
Вернемся на директорию выше и инициализируем проект React с помощью TypeScript:
cd ..
npx create-react-app frontend --template typescript
Полученный вывод будет таким:
Success! Created frontend at Fuel/fuel-project/frontend
Установим необходимые зависимости:
cd frontend
npm install fuels @fuels/react @fuels/connectors @tanstack/react-query
Создадим файл конфигурации:
npx fuels init --contracts ../counter-contract/ --output ./src/sway-api
npx fuels build
Внутри папки fuel-project/frontend/src добавим код, который взаимодействует с нашим контрактом. Изменим код fuel-project/frontend/src/index.tsx, он будет таким:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { FuelProvider } from '@fuels/react';
import {
defaultConnectors,
} from '@fuels/connectors';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<FuelProvider
fuelConfig={{
connectors: defaultConnectors(),
}}
>
<App />
</FuelProvider>
</QueryClientProvider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
Затем изменим файл fuel-project/frontend/src/App.tsx, предвариельно указав свой CONTRACT_ID в "0x...":
import { useEffect, useState } from "react";
import {
useBalance,
useConnectUI,
useIsConnected,
useWallet
} from '@fuels/react';
import { CounterContractAbi__factory } from "./sway-api"
import type { CounterContractAbi } from "./sway-api";
// REPLACE WITH YOUR CONTRACT ID
const CONTRACT_ID =
"0x...";
export default function Home() {
const [contract, setContract] = useState<CounterContractAbi>();
const [counter, setCounter] = useState<number>();
const { connect, isConnecting } = useConnectUI();
const { isConnected } = useIsConnected();
const { wallet } = useWallet();
const { balance } = useBalance({
address: wallet?.address.toAddress(),
assetId: wallet?.provider.getBaseAssetId(),
});
useEffect(() => {
async function getInitialCount(){
if(isConnected && wallet){
const counterContract = CounterContractAbi__factory.connect(CONTRACT_ID, wallet);
await getCount(counterContract);
setContract(counterContract);
}
}
getInitialCount();
}, [isConnected, wallet]);
const getCount = async (counterContract: CounterContractAbi) => {
try{
const { value } = await counterContract.functions
.count()
.get();
setCounter(value.toNumber());
} catch(error) {
console.error(error);
}
}
const onIncrementPressed = async () => {
if (!contract) {
return alert("Contract not loaded");
}
try {
await contract.functions
.increment()
.call();
await getCount(contract);
} catch(error) {
console.error(error);
}
};
return (
<div style={styles.root}>
<div style={styles.container}>
{isConnected ? (
<>
<h3 style={styles.label}>Counter</h3>
<div style={styles.counter}>
{counter ?? 0}
</div>
{balance && balance.toNumber() === 0 ? (
<p>Get testnet funds from the <a target="_blank" rel="noopener noreferrer" href={`https://faucet-testnet.fuel.network/?address=${wallet?.address.toAddress()}`}>Fuel Faucet</a> to increment the counter.</p>
) :
(
<button
onClick={onIncrementPressed}
style={styles.button}
>
Increment Counter
</button>
)
}
<p>Your Fuel Wallet address is:</p>
<p>{wallet?.address.toAddress()}</p>
</>
) : (
<button
onClick={() => {
connect();
}}
style={styles.button}
>
{isConnecting ? 'Connecting' : 'Connect'}
</button>
)}
</div>
</div>
);
}
const styles = {
root: {
display: 'grid',
placeItems: 'center',
height: '100vh',
width: '100vw',
backgroundColor: "black",
} as React.CSSProperties,
container: {
color: "#ffffffec",
display: "flex",
flexDirection: "column",
alignItems: "center",
} as React.CSSProperties,
label: {
fontSize: "28px",
},
counter: {
color: "#a0a0a0",
fontSize: "48px",
},
button: {
borderRadius: "8px",
margin: "24px 0px",
backgroundColor: "#707070",
fontSize: "16px",
color: "#ffffffec",
border: "none",
outline: "none",
height: "60px",
padding: "0 1rem",
cursor: "pointer"
},
}
========================
Внутри каталога fuel-project/frontend запускаем:
npm start
Получаем результат:
Compiled successfully!
You can now view frontend in the browser.
Local: http://localhost:3000
On Your Network: http://192.168.4.48:3000
Note that the development build is not optimized.
To create a production build, use npm run build.
В браузере подключаем свой кошелек Fuel и если у вас есть тестовая сеть ETH на Fuel, вы должны увидеть значение счетчика и кнопку увеличения.
При необходимости воспользуйтесь официальной инструкцией https://docs.fuel.network/guides/counter-dapp/building-a-smart-contract/