Wallet: отправка и получение
Перевод строится совместно обеими сторонами, поэтому отправка представляет собой последовательность вызовов, а не один вызов. См. интерактивные транзакции.
Эти примеры основаны на клиенте из connect and read и запускаются из каталога examples.
InitTxArgs содержит все поля
init_send_tx и process_invoice_tx принимают объект InitTxArgs, и все двенадцать его ключей обязательны при десериализации
кошельком (libwallet/src/api_impl/types.rs:53). Неполный объект отклоняется. init_tx_args в Python-клиенте возвращает полный объект при
каждом вызове:
"""Argument builders for the wallet Owner API transfer calls.
`InitTxArgs` and `IssueInvoiceTxArgs` derive Deserialize without serde defaults, so every key is
required on the wire and a partial object is rejected with a missing-field error. These builders
return every key on every call, which is why the examples pass them rather than hand-written dicts.
Imported by send.py, send_manual.py, invoice.py and payment_proof.py, and re-exported from
epic_wallet.py.
"""
from __future__ import annotations
from typing import Any
EPIC = 100_000_000
"""Freemen in one EPIC. Every amount on the API is an integer count of freemen."""
def init_tx_args(
amount: int,
*,
src_acct_name: str | None = None,
minimum_confirmations: int = 3,
max_outputs: int = 500,
num_change_outputs: int = 1,
selection_strategy_is_use_all: bool = False,
message: str | None = None,
target_slate_version: int | None = None,
ttl_blocks: int | None = None,
payment_proof_recipient_address: str | None = None,
estimate_only: bool = False,
send_args: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build a complete InitTxArgs object for init_send_tx and process_invoice_tx.
All twelve keys are required when the wallet deserializes the object, so this
returns every one of them on every call. `amount` is in freemen.
"""
return {
"src_acct_name": src_acct_name,
"amount": amount,
"minimum_confirmations": minimum_confirmations,
"max_outputs": max_outputs,
"num_change_outputs": num_change_outputs,
"selection_strategy_is_use_all": selection_strategy_is_use_all,
"message": message,
"target_slate_version": target_slate_version,
"ttl_blocks": ttl_blocks,
"payment_proof_recipient_address": payment_proof_recipient_address,
"estimate_only": estimate_only,
"send_args": send_args,
}
def invoice_tx_args(
amount: int,
*,
dest_acct_name: str | None = None,
message: str | None = None,
target_slate_version: int | None = None,
) -> dict[str, Any]:
"""Build a complete IssueInvoiceTxArgs object. All four keys are required."""
return {
"dest_acct_name": dest_acct_name,
"amount": amount,
"message": message,
"target_slate_version": target_slate_version,
}
IssueInvoiceTxArgs работает аналогично и требует четыре обязательных ключа: dest_acct_name, amount, message и target_slate_version. Значения
полей описаны в справочнике Owner API.
Передать управление обменом кошельку
Укажите send_args, и init_send_tx выполнит обмен за вас.
- CLI
- Python
epic-wallet send -m epicbox -d 'esYQ...52chars@epicbox.epiccash.com' -c 3 1.5
# По HTTP к доступному слушателю
epic-wallet send -m http -d http://receiver.example:3415 -c 3 1.5
"""Quote a fee, then send EPIC in a single Owner API call.
Usage: python send.py <amount in EPIC> <destination> [method]
with `epic-wallet owner_api` running.
See https://devdocs.epiccash.com/examples/send-receive
"""
from __future__ import annotations
import os
import sys
from epic_wallet import EPIC, EpicWallet, init_tx_args
def estimate_fee(wallet: EpicWallet, amount_epic: float) -> int:
"""Fee in freemen for a transfer of this size. Selects inputs, reserves nothing."""
slate = wallet.call(
"init_send_tx",
{
"token": wallet.token,
"args": init_tx_args(int(amount_epic * EPIC), estimate_only=True),
},
)
return int(slate["fee"])
def send(
wallet: EpicWallet,
amount_epic: float,
dest: str,
method: str = "epicbox",
) -> str:
"""Build, deliver and lock a transfer in one call. Returns the slate id.
`method` is "epicbox", "http" or "keybase". On the http and keybase paths the
call blocks until the counterparty answers, then finalizes and posts.
"""
slate = wallet.call(
"init_send_tx",
{
"token": wallet.token,
"args": init_tx_args(
int(amount_epic * EPIC),
send_args={
"method": method,
"dest": dest,
"finalize": True,
"post_tx": True,
"fluff": False,
},
),
},
)
return slate["id"]
def main() -> None:
amount_epic = float(sys.argv[1])
dest = sys.argv[2]
method = sys.argv[3] if len(sys.argv) > 3 else "epicbox"
wallet = EpicWallet()
wallet.open_wallet(password=os.environ["EPIC_WALLET_PASSWORD"])
print(f"fee: {estimate_fee(wallet, amount_epic) / 1e8:.8f} EPIC")
print(f"slate id: {send(wallet, amount_epic, dest, method)}")
if __name__ == "__main__":
main()
method принимает http, keybase и epicbox (api/src/owner.rs:749). На путях http и keybase вызов блокируется до ответа
контрагента, затем учитывает finalize и post_tx (api/src/owner.rs:787) — ставьте его в очередь, а не запускайте в потоке
запроса. На пути epicbox вызов публикует слейт (slate) на релей, блокирует входы (inputs) и возвращает
слейт первого раунда отправителя (api/src/owner.rs:761); запущенный им слушатель завершает перевод, когда контрагент
отвечает.
В CLI --min_conf по умолчанию равно 10, а --selection — smallest (src/cmd/wallet_args.rs:143), что соответствует selection_strategy_is_use_all: false. Передайте -c 3
для минимального порога подтверждений, используемого в этих примерах.
Рассчитать комиссию без подтверждения
estimate_only вычисляет комиссию и выбирает входы, не резервируя их.
# Выводит комиссию для обеих стратегий выбора, ничего не отправляет.
epic-wallet send -e -c 3 1.5
В Python estimate_fee() в send.py — это тот же вызов с estimate_only=True, переданным через init_tx_args.
Комиссия зависит от формы транзакции, а не от спроса:
fee = max(4 × outputs + kernels − inputs, 1) × 0.001 EPIC
Рынка комиссий нет, поэтому оценка точная, а не ставка. См. эмиссия и комиссии.
Управление шагами вручную
Используйте это, когда слейт передаётся по вашему собственному транспорту. Это также последовательность, которую выполняет CLI.
init_send_txсобирает слейт.tx_lock_outputsрезервирует входы. Can spend funds С этого момента выходы (outputs) недоступны до завершения перевода или до тех пор, покаcancel_txне освободит их.- Вы доставляете слейт выбранным транспортом.
- Получатель возвращает его подписанным.
finalize_txзавершает агрегированную подпись.post_txтранслирует в сеть транзакцию, содержащуюся в финализированном слейте.
tx_lock_outputs принимает четыре параметра: token, slate, participant_id и addr_to (api/src/owner_rpc_s.rs:897). Отправитель — участник 0.
Ничто не освобождает выходы по таймеру, поэтому всё после блокировки должно находиться внутри
обработчика ошибок, вызывающего cancel_tx. См. что освобождает их.
- CLI
- Python
#!/usr/bin/env bash
# Sender side of a file-transport transfer, with the cancel branch wired in.
# Needs epic-wallet and jq. epic-wallet prompts for the password at each step.
# Usage: bash send-file.sh <amount in EPIC> [slate file]
# See https://devdocs.epiccash.com/examples/send-receive
set -euo pipefail
AMOUNT="${1:?usage: send-file.sh <amount in EPIC> [slate file]}"
SLATE="${2:-slate.tx}"
MIN_CONF="${MIN_CONF:-3}"
TIMEOUT="${TIMEOUT:-600}"
command -v jq >/dev/null || { echo "this script needs jq" >&2; exit 1; }
# Round one. Writes the slate file and reserves the inputs.
epic-wallet send -m file -d "$SLATE" -c "$MIN_CONF" -s smallest "$AMOUNT"
# tr -d '\r' because a Windows jq writes CRLF. Harmless on Linux and macOS.
slate_id=$(jq -r '.id' "$SLATE" | tr -d '\r')
echo "slate $slate_id written to $SLATE, inputs reserved"
# The receiver runs `epic-wallet receive -m file -i <slate>` and returns <slate>.response.
echo "waiting up to ${TIMEOUT}s for $SLATE.response"
deadline=$(( $(date +%s) + TIMEOUT ))
while [ ! -f "$SLATE.response" ]; do
if [ "$(date +%s)" -ge "$deadline" ]; then
echo "no response, releasing the reserved inputs" >&2
epic-wallet cancel -t "$slate_id"
exit 1
fi
sleep 5
done
epic-wallet finalize -m file -i "$SLATE.response"
echo "transfer $slate_id posted"
"""Drive the six steps of a transfer yourself, over your own transport.
Use this when the slate travels by a route the wallet does not implement, or when
you need control over failure handling.
See https://devdocs.epiccash.com/examples/send-receive
"""
from __future__ import annotations
from collections.abc import Callable
from epic_wallet import EPIC, EpicWallet, init_tx_args
def send_manual(
wallet: EpicWallet,
amount_epic: float,
deliver: Callable[[dict], dict],
) -> str:
"""Build, lock, deliver, finalize and post a transfer. Returns the slate id.
`deliver` receives the slate as a dict and returns the counterparty's signed slate.
"""
# 1. Build the first round. Nothing is reserved yet.
slate = wallet.call(
"init_send_tx",
{"token": wallet.token, "args": init_tx_args(int(amount_epic * EPIC))},
)
slate_id = slate["id"]
try:
# 2. Reserve the inputs. The sender is participant 0.
wallet.call(
"tx_lock_outputs",
{
"token": wallet.token,
"slate": slate,
"participant_id": 0,
"addr_to": None,
},
)
# 3. Hand the slate to the counterparty. 4. They return it signed.
returned = deliver(slate)
# 5. Complete the aggregate signature.
final = wallet.call("finalize_tx", {"token": wallet.token, "slate": returned})
# 6. Broadcast. post_tx takes the transaction out of the slate, not the slate.
wallet.call("post_tx", {"token": wallet.token, "tx": final["tx"], "fluff": False})
return slate_id
except Exception:
# Any failure after step 2 leaves outputs reserved. cancel_tx releases them.
wallet.call(
"cancel_tx",
{"token": wallet.token, "tx_id": None, "tx_slate_id": slate_id},
)
raise
Получение
Слушатель обрабатывает сторону получателя:
epic-wallet listen -m epicbox # ставится в очередь, пока вы офлайн
epic-wallet listen # HTTP, требует доступности
Чтобы обработать слейт самостоятельно, используйте метод Foreign API receive_tx. Он работает на порту
3415, не требует токена и принимает четыре позиционных параметра: slate, dest_acct_name, message и addr_from
(api/src/foreign_rpc.rs:359).
- curl
- Python
curl -s -H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"receive_tx\",\"params\":[$(cat slate.json),null,null,null],\"id\":1}" \
http://127.0.0.1:3415/v2/foreign | jq '.result.Ok'
"""Call the wallet Foreign API: add our part to an inbound slate.
The Foreign API takes no token and listens on its own port. Override the default with
EPIC_FOREIGN_URL=http://127.0.0.1:3415/v2/foreign
See https://devdocs.epiccash.com/examples/send-receive
"""
from __future__ import annotations
import json
import os
import sys
from typing import Any
import requests
FOREIGN_URL = os.environ.get("EPIC_FOREIGN_URL", "http://127.0.0.1:3415/v2/foreign")
def foreign_rpc(method: str, params: list[Any]) -> Any:
"""Call a Foreign API method. Params are positional on this surface."""
payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
response = requests.post(FOREIGN_URL, json=payload, timeout=300)
response.raise_for_status()
body = response.json()
if "error" in body:
raise RuntimeError(f"JSON-RPC error: {body['error']}")
result = body["result"]
if "Err" in result:
raise RuntimeError(f"Wallet error: {result['Err']}")
return result["Ok"]
def receive_tx(
slate: dict,
dest_acct_name: str | None = None,
message: str | None = None,
addr_from: str | None = None,
) -> dict:
"""Return the slate with our output and partial signature added.
Four positional params, in this order. `dest_acct_name` names the account the
funds are credited to.
"""
return foreign_rpc("receive_tx", [slate, dest_acct_name, message, addr_from])
if __name__ == "__main__":
# Read the sender's slate file, write the signed slate back alongside it.
path = sys.argv[1]
with open(path, encoding="utf-8") as handle:
inbound = json.load(handle)
with open(f"{path}.response", "w", encoding="utf-8") as handle:
json.dump(receive_tx(inbound), handle)
print(f"signed slate written to {path}.response")
Foreign API не требует учётных данных. Ограничьте доступ к нему на уровне прокси и см. аутентификация.
Поиск и устранение застрявших переводов
- CLI
- Python
epic-wallet info # если доступно к трате меньше общего — что-то зарезервировано
epic-wallet txs # искать Sent (Created) с Confirmed? false
epic-wallet cancel -i 42 # освободить входы этого перевода
epic-wallet scan # пересканировать, если цифры по-прежнему расходятся с цепочкой
"""Find outbound transfers still holding reserved inputs, and release them.
Usage: python clear_stuck.py [--cancel], with `epic-wallet owner_api` running.
Without --cancel it only lists. See https://devdocs.epiccash.com/guides/stuck-transactions
"""
from __future__ import annotations
import os
import sys
from epic_wallet import EpicWallet
def stuck_transfers(wallet: EpicWallet) -> list[dict]:
"""Outbound transfers that are built but not confirmed."""
result = wallet.call(
"retrieve_txs",
{
"token": wallet.token,
"refresh_from_node": True,
"tx_id": None,
"tx_slate_id": None,
"limit": 500,
"offset": 0,
"sort_order": "desc",
},
)
return [t for t in result["txs"] if t["tx_type"] == "TxSentCreated" and not t["confirmed"]]
def cancel(wallet: EpicWallet, tx_id: int) -> None:
"""Release the inputs a transfer reserved. Needs a reachable node."""
wallet.call("cancel_tx", {"token": wallet.token, "tx_id": tx_id, "tx_slate_id": None})
def main() -> None:
wallet = EpicWallet()
wallet.open_wallet(password=os.environ["EPIC_WALLET_PASSWORD"])
for tx in stuck_transfers(wallet):
print(f"{tx['id']:>5} {tx['creation_ts']} {tx['tx_slate_id']}")
if "--cancel" in sys.argv:
cancel(wallet, tx["id"])
print(f"{'':>5} cancelled")
if __name__ == "__main__":
main()
Для отмены необходим доступный узел и перевод, который ещё не подтверждён. Алгоритм принятия решения описан в застрявшие транзакции.
Инвойсы
Получатель запрашивает сумму, а плательщик её финансирует. Криптография та же; меняется инициатор.
finalize_invoice_tx — метод Foreign API, поэтому получатель финализирует там и публикует через Owner API.
- CLI
- Python
epic-wallet invoice -d invoice.tx 2 # payee
epic-wallet pay -c 3 -i invoice.tx # плательщик, записывает invoice.tx.response
epic-wallet finalize -i invoice.tx.response # payee
"""Issue an invoice as the payee, and fund one as the payer.
The payee asks for an amount and the payer's wallet supplies the inputs, so the
initiator is reversed compared with a send.
See https://devdocs.epiccash.com/examples/send-receive
"""
from __future__ import annotations
from epic_wallet import EPIC, EpicWallet, init_tx_args, invoice_tx_args
from receive import foreign_rpc
def issue(wallet: EpicWallet, amount_epic: float, message: str | None = None) -> dict:
"""Payee, Owner API: create the invoice slate to hand to the payer."""
return wallet.call(
"issue_invoice_tx",
{
"token": wallet.token,
"args": invoice_tx_args(int(amount_epic * EPIC), message=message),
},
)
def fund(wallet: EpicWallet, invoice_slate: dict) -> dict:
"""Payer, Owner API: add inputs and a partial signature, reserving the payer's outputs.
The amount comes from the invoice slate. The InitTxArgs fields that apply here are
the selection ones: minimum_confirmations, max_outputs, num_change_outputs and
selection_strategy_is_use_all.
"""
return wallet.call(
"process_invoice_tx",
{
"token": wallet.token,
"slate": invoice_slate,
"args": init_tx_args(int(invoice_slate["amount"])),
},
)
def complete(wallet: EpicWallet, funded_slate: dict) -> dict:
"""Payee: finalize on the Foreign API, then post from the Owner API."""
final = foreign_rpc("finalize_invoice_tx", [funded_slate])
wallet.call("post_tx", {"token": wallet.token, "tx": final["tx"], "fluff": False})
return final
Доказательства платежа
Запрашивайте доказательство платежа при отправке — добавить его впоследствии невозможно. Epicbox передаёт слейт версии V2, а доказательство платежа является полем V3, поэтому при необходимости отправляйте через HTTP. См. доказательства платежа.
- CLI
- Python
epic-wallet send --request_payment_proof \
--proof_address <recipient proof address> \
-m http -d http://receiver.example:3415 -c 3 1
epic-wallet export_proof -i 42 proof.json
epic-wallet verify_proof proof.json
"""Request a payment proof at send time, then retrieve and verify it.
A proof is requested when the transfer is built and cannot be added afterwards.
See https://devdocs.epiccash.com/concepts/payment-proofs
"""
from __future__ import annotations
from epic_wallet import EPIC, EpicWallet, init_tx_args
def send_with_proof(
wallet: EpicWallet,
amount_epic: float,
dest: str,
recipient_proof_address: str,
) -> str:
"""Send over http, requesting a proof addressed to the recipient. Returns the slate id."""
slate = wallet.call(
"init_send_tx",
{
"token": wallet.token,
"args": init_tx_args(
int(amount_epic * EPIC),
payment_proof_recipient_address=recipient_proof_address,
send_args={
"method": "http",
"dest": dest,
"finalize": True,
"post_tx": True,
"fluff": False,
},
),
},
)
return slate["id"]
def retrieve(wallet: EpicWallet, slate_id: str) -> dict:
"""The stored proof for a completed transfer."""
return wallet.call(
"retrieve_payment_proof",
{
"token": wallet.token,
"refresh_from_node": True,
"tx_id": None,
"tx_slate_id": slate_id,
},
)
def verify(wallet: EpicWallet, proof: dict) -> list[bool]:
"""Verify a proof. Returns two booleans."""
return wallet.call("verify_payment_proof", {"token": wallet.token, "proof": proof})
verify_payment_proof возвращает два булевых значения, описанных в доказательства платежа.