钱包:发送与接收
转账由双方共同构建,因此发送是一个序列而非单次调用。参见交互式交易。
以下示例基于连接与读取中的客户端,并在examples目录下运行。
InitTxArgs包含每个字段
init_send_tx和process_invoice_tx均接受一个InitTxArgs对象,钱包反序列化时要求其全部十二个键(libwallet/src/api_impl/types.rs:53)。不完整的对象会被拒绝。Python客户端中的init_tx_args每次调用均返回完整对象:
"""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发布到中继,锁定输入(input)并返回发送方的第一轮slate(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计算手续费并选择输入(input),不预留任何内容。
# 打印两种选择策略的手续费,不实际发送。
epic-wallet send -e -c 3 1.5
在Python中,send.py里的estimate_fee()是相同的调用,通过init_tx_args传入estimate_only=True。
手续费是交易形态的函数,与需求无关:
fee = max(4 × outputs + kernels − inputs, 1) × 0.001 EPIC
不存在手续费市场,因此估算值是精确值而非竞价。参见发行与手续费。
手动执行各步骤
当slate通过自有传输方式传递时使用此方式。这也是CLI所执行的序列。
init_send_tx构建slate。tx_lock_outputs预留输入(input)。Can spend funds从此刻起,相关输出(output)不可用,直到转账完成或cancel_tx释放它们。- 通过任意传输方式交付slate。
- 接收方返回已签名的slate。
finalize_tx完成聚合签名。post_tx广播最终确认的slate中携带的交易。
tx_lock_outputs接受四个参数:token、slate、participant_id和addr_to(api/src/owner_rpc_s.rs:897)。发送方为参与者0。没有任何机制会按计时器释放输出(output),因此锁定之后的所有操作都应放在调用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,需要可达性
如需自行处理slate,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传输slate 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返回两个布尔值,详见支付证明。