Wallet: send and receive
A transfer is built jointly by both parties, so sending is a sequence rather than a single call. See interactive transactions.
These examples build on the client from connect and read, and run from
the examples directory.
InitTxArgs carries every field
init_send_tx and process_invoice_tx both take an InitTxArgs object, and all twelve of its keys
are required when the wallet deserializes it
(libwallet/src/api_impl/types.rs:53). A partial object is
rejected. init_tx_args in the Python client returns a complete one on every call:
"""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 works the same way, with four required keys: dest_acct_name, amount,
message and target_slate_version. Field meanings are in
the Owner API reference.
Let the wallet drive the exchange
Supply send_args and init_send_tx performs the exchange for you.
- CLI
- Python
epic-wallet send -m epicbox -d 'esYQ...52chars@epicbox.epiccash.com' -c 3 1.5
# Over HTTP to a reachable listener
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 accepts http, keybase and epicbox
(api/src/owner.rs:749). On the http and keybase paths the
call blocks until the counterparty answers, then honours finalize and post_tx
(api/src/owner.rs:787), so queue it rather than running it on a
request thread. On the epicbox path the call publishes the slate to the relay, locks the inputs
and returns the sender's round-one slate
(api/src/owner.rs:761); the listener it starts completes the
transfer when the counterparty replies.
On the CLI, --min_conf defaults to 10 and --selection to smallest
(src/cmd/wallet_args.rs:143), which is
selection_strategy_is_use_all: false. Pass -c 3 for the confirmation floor these examples use.
Quote a fee without committing
estimate_only computes the fee and selects inputs without reserving anything.
# Prints the fee for both selection strategies and sends nothing.
epic-wallet send -e -c 3 1.5
In Python, estimate_fee() in send.py is the same call with
estimate_only=True passed through init_tx_args.
The fee is a function of transaction shape, not demand:
fee = max(4 × outputs + kernels − inputs, 1) × 0.001 EPIC
There is no fee market, so the estimate is exact rather than a bid. See emission and fees.
Driving the steps yourself
Use this when the slate travels over your own transport. This is also the sequence the CLI runs.
init_send_txbuilds the slate.tx_lock_outputsreserves the inputs. Can spend funds From here the outputs are unavailable until the transfer completes orcancel_txreleases them.- You deliver the slate, by whichever transport.
- The receiver returns it signed.
finalize_txcompletes the aggregate signature.post_txbroadcasts the transaction carried in the finalized slate.
tx_lock_outputs takes four parameters, token, slate, participant_id and addr_to
(api/src/owner_rpc_s.rs:897). The sender is participant 0.
Nothing releases outputs on a timer, so everything after the lock belongs inside error handling that
calls cancel_tx. See what releases them.
- 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
Receiving
A listener handles the receiving side:
epic-wallet listen -m epicbox # queues while you are offline
epic-wallet listen # HTTP, requires reachability
To process a slate yourself, the Foreign API method is receive_tx. It is on port
3415, takes no token, and takes four positional params,
slate, dest_acct_name, message and 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")
The Foreign API takes no credential. Restrict access to it at the proxy, and see authentication.
Finding and clearing stuck transfers
- CLI
- Python
epic-wallet info # spendable below total means something is reserved
epic-wallet txs # look for Sent (Created) with Confirmed? false
epic-wallet cancel -i 42 # release that transfer's inputs
epic-wallet scan # rescan if the figures still disagree with the chain
"""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()
Cancelling needs a reachable node and a transfer that is not yet confirmed. The decision procedure is in stuck transactions.
Invoices
The payee requests an amount and the payer funds it. The cryptography is the same; the initiator
changes. finalize_invoice_tx is a Foreign API method, so the payee finalizes there and posts from
the Owner API.
- CLI
- Python
epic-wallet invoice -d invoice.tx 2 # payee
epic-wallet pay -c 3 -i invoice.tx # payer, writes 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
Payment proofs
Request the proof when you send; it cannot be added afterwards. Epicbox carries slate V2 and a payment proof is a V3 field, so send over HTTP when you need one. See payment proofs.
- 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 returns two booleans, covered in
payment proofs.