В этом руководстве в пошаговом порядке описывается создание, отправка и проверка транзакций, отправленных в Aptos Blockchain:
Создайте представление учетной записи.
Каждая учетная запись Aptos имеет уникальный адрес учетной записи. Владелец этой учетной записи владеет парой открытого и закрытого ключей, которая сопоставляется с адресом учетной записи Aptos, и, в свою очередь, ключом аутентификации, хранящимся в этой учетной записи.
ПРИМЕЧАНИЕ
Подробнее об учетных записях Aptos см. в разделе .
2. Подготовьте оболочку для интерфейсов REST.
Aptos предоставляет для взаимодействия с блокчейном. Эти шаги подготавливают оболочки вокруг этого API для получения информации об учетной записи, а также для создания транзакции, ее подписания и отправки.
3. Подготовьте оболочку для интерфейса Faucet.
Используя интерфейс Faucet в Aptos devnet, этот обучающий код автоматически создает аккаунт с адресом 0x1 и пополняет его.
4. Объедините вышеуказанные оболочки в приложение, выполните и проверьте.
Прежде чем начать
Сначала убедитесь, что вы выполнили следующие шаги, чтобы вы могли запустить руководство.
3. Проверьте ветку devnet, используя git checkout --track origin/devnet.
4. Запустите bash-скрипт scripts/dev_setup.sh , как показано ниже. Это подготовит вашу среду разработки.
./scripts/dev_setup.sh
5. Обновите текущую среду оболочки.
source ~/.cargo/env
Теперь, когда ваша среда разработки готова, вы можете запустить это руководство.
Источник GitHub
Перейдите по ссылкам ниже, чтобы получить доступ к исходному коду руководства:
Шаг 1: Создайте представление учетной записи
class Account:
"""Represents an account as well as the private, public key-pair for the Aptos blockchain."""
def __init__(self, seed: bytes = None) -> None:
if seed is None:
self.signing_key = SigningKey.generate()
else:
self.signing_key = SigningKey(seed)
def address(self) -> str:
"""Returns the address associated with the given account"""
return self.auth_key()
def auth_key(self) -> str:
"""Returns the auth_key for the associated account"""
hasher = hashlib.sha3_256()
hasher.update(self.signing_key.verify_key.encode() + b'\x00')
return hasher.hexdigest()
def pub_key(self) -> str:
"""Returns the public key for the associated account"""
return self.signing_key.verify_key.encode().hex()
pub struct Account {
signing_key: SecretKey,
}
impl Account {
/// Represents an account as well as the private, public key-pair for the Aptos blockchain.
pub fn new(priv_key_bytes: Option<Vec<u8>>) -> Self {
let signing_key = match priv_key_bytes {
Some(key) => SecretKey::from_bytes(&key).unwrap(),
None => SecretKey::generate(&mut rand::rngs::StdRng::from_seed(OsRng.gen())),
};
Account { signing_key }
}
/// Returns the address associated with the given account
pub fn address(&self) -> String {
self.auth_key()
}
/// Returns the auth_key for the associated account
pub fn auth_key(&self) -> String {
let mut sha3 = Sha3::v256();
sha3.update(PublicKey::from(&self.signing_key).as_bytes());
sha3.update(&vec![0u8]);
let mut output = [0u8; 32];
sha3.finalize(&mut output);
hex::encode(output)
}
/// Returns the public key for the associated account
pub fn pub_key(&self) -> String {
hex::encode(PublicKey::from(&self.signing_key).as_bytes())
}
}
/** A subset of the fields of a TransactionRequest, for this tutorial */
export type TxnRequest = Record<string, any> & { sequence_number: string };
/** Represents an account as well as the private, public key-pair for the Aptos blockchain */
export class Account {
signingKey: Nacl.SignKeyPair;
constructor(seed?: Uint8Array | undefined) {
if (seed) {
this.signingKey = Nacl.sign.keyPair.fromSeed(seed);
} else {
this.signingKey = Nacl.sign.keyPair();
}
}
/** Returns the address associated with the given account */
address(): string {
return this.authKey();
}
/** Returns the authKey for the associated account */
authKey(): string {
let hash = SHA3.sha3_256.create();
hash.update(Buffer.from(this.signingKey.publicKey));
hash.update("\x00");
return hash.hex();
}
/** Returns the public key for the associated account */
pubKey(): string {
return Buffer.from(this.signingKey.publicKey).toString("hex");
}
}
Шаг 2: REST интерфейс
Хотя данные из интерфейса REST можно считывать напрямую, следующие примеры кода демонстрируют более эргономичный подход при использовании интерфейса REST для:
Получение данных реестра из полной ноды, включая данные учетной записи и ресурсов учетной записи.
Построение подписанных транзакций, представленных в формате JSON.
class RestClient:
"""A wrapper around the Aptos-core Rest API"""
def __init__(self, url: str) -> None:
self.url = url
#[derive(Clone)]
pub struct RestClient {
url: String,
}
impl RestClient {
/// A wrapper around the Aptos-core Rest API
pub fn new(url: String) -> Self {
Self { url }
}
/** A wrapper around the Aptos-core Rest API */
export class RestClient {
url: string;
constructor(url: string) {
this.url = url;
}
Шаг 2.1: Чтение аккаунта
Ниже приведены оболочки для запроса данных учетной записи.
def account(self, account_address: str) -> Dict[str, str]:
"""Returns the sequence number and authentication key for an account"""
response = requests.get(f"{self.url}/accounts/{account_address}")
assert response.status_code == 200, f"{response.text} - {account_address}"
return response.json()
def account_resource(self, account_address: str, resource_type: str) -> Optional[Dict[str, Any]]:
response = requests.get(f"{self.url}/accounts/{account_address}/resource/{resource_type}")
if response.status_code == 404:
return None
assert response.status_code == 200, response.text
return response.json()
/// Returns the sequence number and authentication key for an account
pub fn account(&self, account_address: &str) -> serde_json::Value {
let res =
reqwest::blocking::get(format!("{}/accounts/{}", self.url, account_address)).unwrap();
if res.status() != 200 {
assert_eq!(
res.status(),
200,
"{} - {}",
res.text().unwrap_or("".to_string()),
account_address,
);
}
res.json().unwrap()
}
/// Returns all resources associated with the account
pub fn account_resource(
&self,
account_address: &str,
resource_type: &str,
) -> Option<serde_json::Value> {
let res = reqwest::blocking::get(format!(
"{}/accounts/{}/resource/{}",
self.url, account_address, resource_type,
))
.unwrap();
if res.status() == 404 {
None
} else if res.status() != 200 {
assert_eq!(
res.status(),
200,
"{} - {}",
res.text().unwrap_or("".to_string()),
account_address,
);
unreachable!()
} else {
Some(res.json().unwrap())
}
}
/** Returns the sequence number and authentication key for an account */
async account(accountAddress: string): Promise<Record<string, string> & { sequence_number: string }> {
const response = await fetch(`${this.url}/accounts/${accountAddress}`, { method: "GET" });
if (response.status != 200) {
assert(response.status == 200, await response.text());
}
return await response.json();
}
/** Returns all resources associated with the account */
async accountResource(accountAddress: string, resourceType: string): Promise<any> {
const response = await fetch(`${this.url}/accounts/${accountAddress}/resource/${resourceType}`, { method: "GET" });
if (response.status == 404) {
return null;
}
if (response.status != 200) {
assert(response.status == 200, await response.text());
}
return await response.json();
}
Шаг 2.2: Отправка транзакции
Далее демонстрируются основные функции построения, подписания и ожидания транзакции.
def generate_transaction(self, sender: str, payload: Dict[str, Any]) -> Dict[str, Any]:
"""Generates a transaction request that can be submitted to produce a raw transaction that
can be signed, which upon being signed can be submitted to the blockchain. """
account_res = self.account(sender)
seq_num = int(account_res["sequence_number"])
txn_request = {
"sender": f"0x{sender}",
"sequence_number": str(seq_num),
"max_gas_amount": "2000",
"gas_unit_price": "1",
"gas_currency_code": "XUS",
"expiration_timestamp_secs": str(int(time.time()) + 600),
"payload": payload,
}
return txn_request
def sign_transaction(self, account_from: Account, txn_request: Dict[str, Any]) -> Dict[str, Any]:
"""Converts a transaction request produced by `generate_transaction` into a properly signed
transaction, which can then be submitted to the blockchain."""
res = requests.post(f"{self.url}/transactions/signing_message", json=txn_request)
assert res.status_code == 200, res.text
to_sign = bytes.fromhex(res.json()["message"][2:])
signature = account_from.signing_key.sign(to_sign).signature
txn_request["signature"] = {
"type": "ed25519_signature",
"public_key": f"0x{account_from.pub_key()}",
"signature": f"0x{signature.hex()}",
}
return txn_request
def submit_transaction(self, txn: Dict[str, Any]) -> Dict[str, Any]:
"""Submits a signed transaction to the blockchain."""
headers = {'Content-Type': 'application/json'}
response = requests.post(f"{self.url}/transactions", headers=headers, json=txn)
assert response.status_code == 202, f"{response.text} - {txn}"
return response.json()
def execute_transaction_with_payload(self, account_from: Account, payload: Dict[str, Any]) -> Dict[str, Any]:
"""Execute a transaction for the given payload."""
txn_request = self.generate_transaction(account_from.address(), payload)
signed_txn = self.sign_transaction(account_from, txn_request)
return self.submit_transaction(signed_txn)
def transaction_pending(self, txn_hash: str) -> bool:
response = requests.get(f"{self.url}/transactions/{txn_hash}")
if response.status_code == 404:
return True
assert response.status_code == 200, f"{response.text} - {txn_hash}"
return response.json()["type"] == "pending_transaction"
def wait_for_transaction(self, txn_hash: str) -> None:
"""Waits up to 10 seconds for a transaction to move past pending state."""
count = 0
while self.transaction_pending(txn_hash):
assert count < 10, f"transaction {txn_hash} timed out"
time.sleep(1)
count += 1
response = requests.get(f"{self.url}/transactions/{txn_hash}")
assert "success" in response.json(), f"{response.text} - {txn_hash}"
/// Generates a transaction request that can be submitted to produce a raw transaction that can be signed, which upon being signed can be submitted to the blockchain.
pub fn generate_transaction(
&self,
sender: &str,
payload: serde_json::Value,
) -> serde_json::Value {
let account_res = self.account(sender);
let seq_num = account_res
.get("sequence_number")
.unwrap()
.as_str()
.unwrap()
.parse::<u64>()
.unwrap();
// Unix timestamp, in seconds + 10 minutes
let expiration_time_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs()
+ 600;
serde_json::json!({
"sender": format!("0x{}", sender),
"sequence_number": seq_num.to_string(),
"max_gas_amount": "1000",
"gas_unit_price": "1",
"gas_currency_code": "XUS",
"expiration_timestamp_secs": expiration_time_secs.to_string(),
"payload": payload,
})
}
/// Converts a transaction request produced by `generate_transaction` into a properly signed transaction, which can then be submitted to the blockchain.
pub fn sign_transaction(
&self,
account_from: &mut Account,
mut txn_request: serde_json::Value,
) -> serde_json::Value {
let res = reqwest::blocking::Client::new()
.post(format!("{}/transactions/signing_message", self.url))
.body(txn_request.to_string())
.send()
.unwrap();
if res.status() != 200 {
assert_eq!(
res.status(),
200,
"{} - {}",
res.text().unwrap_or("".to_string()),
txn_request.as_str().unwrap_or(""),
);
}
let body: serde_json::Value = res.json().unwrap();
let to_sign_hex = Box::new(body.get("message").unwrap().as_str()).unwrap();
let to_sign = hex::decode(&to_sign_hex[2..]).unwrap();
let signature: String = ExpandedSecretKey::from(&account_from.signing_key)
.sign(&to_sign, &PublicKey::from(&account_from.signing_key))
.encode_hex();
let signature_payload = serde_json::json!({
"type": "ed25519_signature",
"public_key": format!("0x{}", account_from.pub_key()),
"signature": format!("0x{}", signature),
});
txn_request
.as_object_mut()
.unwrap()
.insert("signature".to_string(), signature_payload);
txn_request
}
/// Submits a signed transaction to the blockchain.
pub fn submit_transaction(&self, txn_request: &serde_json::Value) -> serde_json::Value {
let res = reqwest::blocking::Client::new()
.post(format!("{}/transactions", self.url))
.body(txn_request.to_string())
.header("Content-Type", "application/json")
.send()
.unwrap();
if res.status() != 202 {
assert_eq!(
res.status(),
202,
"{} - {}",
res.text().unwrap_or("".to_string()),
txn_request.as_str().unwrap_or(""),
);
}
res.json().unwrap()
}
/// Submits a signed transaction to the blockchain.
pub fn execution_transaction_with_payload(
&self,
account_from: &mut Account,
payload: serde_json::Value,
) -> String {
let txn_request = self.generate_transaction(&account_from.address(), payload);
let signed_txn = self.sign_transaction(account_from, txn_request);
let res = self.submit_transaction(&signed_txn);
res.get("hash").unwrap().as_str().unwrap().to_string()
}
pub fn transaction_pending(&self, transaction_hash: &str) -> bool {
let res = reqwest::blocking::get(format!("{}/transactions/{}", self.url, transaction_hash))
.unwrap();
if res.status() == 404 {
return true;
}
if res.status() != 200 {
assert_eq!(
res.status(),
200,
"{} - {}",
res.text().unwrap_or("".to_string()),
transaction_hash,
);
}
res.json::<serde_json::Value>()
.unwrap()
.get("type")
.unwrap()
.as_str()
.unwrap()
== "pending_transaction"
}
/// Waits up to 10 seconds for a transaction to move past pending state.
pub fn wait_for_transaction(&self, txn_hash: &str) {
let mut count = 0;
while self.transaction_pending(txn_hash) {
assert!(count < 10, "transaction {} timed out", txn_hash);
thread::sleep(Duration::from_secs(1));
count += 1;
}
}
/** Generates a transaction request that can be submitted to produce a raw transaction that
can be signed, which upon being signed can be submitted to the blockchain. */
async generateTransaction(sender: string, payload: Record<string, any>): Promise<TxnRequest> {
const account = await this.account(sender);
const seqNum = parseInt(account["sequence_number"]);
return {
sender: `0x${sender}`,
sequence_number: seqNum.toString(),
max_gas_amount: "2000",
gas_unit_price: "1",
// Unix timestamp, in seconds + 10 minutes
expiration_timestamp_secs: (Math.floor(Date.now() / 1000) + 600).toString(),
payload: payload,
};
}
/** Converts a transaction request produced by `generate_transaction` into a properly signed
transaction, which can then be submitted to the blockchain. */
async signTransaction(accountFrom: Account, txnRequest: TxnRequest): Promise<TxnRequest> {
const response = await fetch(`${this.url}/transactions/signing_message`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(txnRequest),
});
if (response.status != 200) {
assert(response.status == 200, (await response.text()) + " - " + JSON.stringify(txnRequest));
}
const result: Record<string, any> & { message: string } = await response.json();
const toSign = Buffer.from(result["message"].substring(2), "hex");
const signature = Nacl.sign(toSign, accountFrom.signingKey.secretKey);
const signatureHex = Buffer.from(signature).toString("hex").slice(0, 128);
txnRequest["signature"] = {
type: "ed25519_signature",
public_key: `0x${accountFrom.pubKey()}`,
signature: `0x${signatureHex}`,
};
return txnRequest;
}
/** Submits a signed transaction to the blockchain. */
async submitTransaction(txnRequest: TxnRequest): Promise<Record<string, any>> {
const response = await fetch(`${this.url}/transactions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(txnRequest),
});
if (response.status != 202) {
assert(response.status == 202, (await response.text()) + " - " + JSON.stringify(txnRequest));
}
return await response.json();
}
async executeTransactionWithPayload(accountFrom: Account, payload: Record<string, any>): Promise<string> {
const txnRequest = await this.generateTransaction(accountFrom.address(), payload);
const signedTxn = await this.signTransaction(accountFrom, txnRequest);
const res = await this.submitTransaction(signedTxn);
return res["hash"];
}
async transactionPending(txnHash: string): Promise<boolean> {
const response = await fetch(`${this.url}/transactions/${txnHash}`, { method: "GET" });
if (response.status == 404) {
return true;
}
if (response.status != 200) {
assert(response.status == 200, await response.text());
}
return (await response.json())["type"] == "pending_transaction";
}
/** Waits up to 10 seconds for a transaction to move past pending state */
async waitForTransaction(txnHash: string) {
let count = 0;
while (await this.transactionPending(txnHash)) {
assert(count < 10);
await new Promise((resolve) => setTimeout(resolve, 1000));
count += 1;
if (count >= 10) {
throw new Error(`Waiting for transaction ${txnHash} timed out!`);
}
}
}
Шаг 2.3: Логика, специфичная для конкретного приложения
Ниже показано, как считывать данные из блокчейна и как отправлять конкретную транзакцию.
def account_balance(self, account_address: str) -> Optional[int]:
"""Returns the test coin balance associated with the account"""
return self.account_resource(account_address, "0x1::Coin::CoinStore<0x1::TestCoin::TestCoin>")
def transfer(self, account_from: Account, recipient: str, amount: int) -> str:
"""Transfer a given coin amount from a given Account to the recipient's account address.
Returns the sequence number of the transaction used to transfer."""
payload = {
"type": "script_function_payload",
"function": "0x1::Coin::transfer",
"type_arguments": ["0x1::TestCoin::TestCoin"],
"arguments": [
f"0x{recipient}",
str(amount),
]
}
txn_request = self.generate_transaction(account_from.address(), payload)
signed_txn = self.sign_transaction(account_from, txn_request)
res = self.submit_transaction(signed_txn)
return str(res["hash"])
/// Returns the test coin balance associated with the account
pub fn account_balance(&self, account_address: &str) -> Option<u64> {
self.account_resource(account_address, "0x1::Coin::CoinStore<0x1::TestCoin::TestCoin>")
.unwrap()["data"]["coin"]["value"]
.as_str()
.and_then(|s| s.parse::<u64>().ok())
}
/// Transfer a given coin amount from a given Account to the recipient's account address.
/// Returns the sequence number of the transaction used to transfer
pub fn transfer(&self, account_from: &mut Account, recipient: &str, amount: u64) -> String {
let payload = serde_json::json!({
"type": "script_function_payload",
"function": "0x1::Coin::transfer",
"type_arguments": ["0x1::TestCoin::TestCoin"],
"arguments": [format!("0x{}", recipient), amount.to_string()]
});
let txn_request = self.generate_transaction(&account_from.address(), payload);
let signed_txn = self.sign_transaction(account_from, txn_request);
let res = self.submit_transaction(&signed_txn);
res.get("hash").unwrap().as_str().unwrap().to_string()
}
}
/** Returns the test coin balance associated with the account */
async accountBalance(accountAddress: string): Promise<number | null> {
const resource = await this.accountResource(accountAddress, "0x1::Coin::CoinStore<0x1::TestCoin::TestCoin>");
if (resource == null) {
return null;
}
return parseInt(resource["data"]["coin"]["value"]);
}
/** Transfer a given coin amount from a given Account to the recipient's account address.
Returns the sequence number of the transaction used to transfer. */
async transfer(accountFrom: Account, recipient: string, amount: number): Promise<string> {
const payload: { function: string; arguments: string[]; type: string; type_arguments: any[] } = {
type: "script_function_payload",
function: "0x1::Coin::transfer",
type_arguments: ["0x1::TestCoin::TestCoin"],
arguments: [`0x${recipient}`, amount.toString()],
};
const txnRequest = await this.generateTransaction(accountFrom.address(), payload);
const signedTxn = await this.signTransaction(accountFrom, txnRequest);
const res = await this.submitTransaction(signedTxn);
return res["hash"].toString();
}
}
Шаг 3: Faucet интерфейс
Сборщики Aptos Blockchain выдают учетным записям тестовые токены. Эти тестовые токены можно использовать для тестирования, например, для оплаты газа или передачи токенов между пользователями. Aptos Faucet также может создавать учетные записи, если они не существуют. Для интерфейса Aptos Faucet требуется открытый ключ, представленный в виде строки в шестнадцатеричном коде.
class FaucetClient:
"""Faucet creates and funds accounts. This is a thin wrapper around that."""
def __init__(self, url: str, rest_client: RestClient) -> None:
self.url = url
self.rest_client = rest_client
def fund_account(self, address: str, amount: int) -> None:
"""This creates an account if it does not exist and mints the specified amount of
coins into that account."""
txns = requests.post(f"{self.url}/mint?amount={amount}&address={address}")
assert txns.status_code == 200, txns.text
for txn_hash in txns.json():
self.rest_client.wait_for_transaction(txn_hash)
pub struct FaucetClient {
url: String,
rest_client: RestClient,
}
impl FaucetClient {
/// Faucet creates and funds accounts. This is a thin wrapper around that.
pub fn new(url: String, rest_client: RestClient) -> Self {
Self { url, rest_client }
}
/// This creates an account if it does not exist and mints the specified amount of coins into that account.
pub fn fund_account(&self, auth_key: &str, amount: u64) {
let res = reqwest::blocking::Client::new()
.post(format!(
"{}/mint?amount={}&auth_key={}",
self.url, amount, auth_key
))
.send()
.unwrap();
if res.status() != 200 {
assert_eq!(
res.status(),
200,
"{}",
res.text().unwrap_or("".to_string()),
);
}
for txn_hash in res.json::<serde_json::Value>().unwrap().as_array().unwrap() {
self.rest_client
.wait_for_transaction(txn_hash.as_str().unwrap())
}
}
}
/** Faucet creates and funds accounts. This is a thin wrapper around that. */
export class FaucetClient {
url: string;
restClient: RestClient;
constructor(url: string, restClient: RestClient) {
this.url = url;
this.restClient = restClient;
}
/** This creates an account if it does not exist and mints the specified amount of
coins into that account */
async fundAccount(address: string, amount: number) {
const url = `${this.url}/mint?amount=${amount}&address=${address}`;
const response = await fetch(url, { method: "POST" });
if (response.status != 200) {
assert(response.status == 200, await response.text());
}
const tnxHashes = (await response.json()) as Array<string>;
for (const tnxHash of tnxHashes) {
await this.restClient.waitForTransaction(tnxHash);
}
}
}
Шаг 4: Запустите приложение
Наконец, мы можем запустить приложение и проверить вывод.
Для Python3:
Перейдите cd в каталог aptos-core/developer-docs-site/static/examples/python .
Установите необходимые библиотеки: pip3 install -r requirements.txt.
Запустите пример: python3 first_transaction.py.
Для Rust:
Перейдите cd в каталог aptos-core/developer-docs-site/static/examples/rust .
Выполните пример: cargo run --bin first-transaction (убедитесь, что вы используете first-transaction , а не first_transaction).
Для Typescript:
Перейдите cd в каталог aptos-core/developer-docs-site/static/examples/typescript .
Результат показывает, что Боб получил 1000 монет от Алисы. Алиса заплатила 43 монеты за газ.
Проверка
Данные можно проверить, посетив либо интерфейс REST, либо программу анализа:
ПРИМЕЧАНИЕ
См. first_transaction.py код в данного руководства.
См. first_transaction.rs код в данного руководства.
См. first_transaction.ts код в данного руководства.
Эти шаги создают представление учетной записи. См. также разделы и .
Убедитесь, что выполнены предварительные требования, описанные в разделе .
Убедитесь, что выполнены предварительные требования, описанные в разделе .
Убедитесь, что выполнены предварительные требования, описанные в разделе .
Учетная запись Алисы через .
Учетная запись Боба через .
Aptos devnet периодически перезапускается, поэтому приведенные выше ссылки могут не работать. Попробуйте выполнить инструкцию самостоятельно, а затем проверьте учетные записи в .