Guide
Accept and send payments
Integrate TRAI deposits and withdrawals on a server with the wallet daemon. Addresses per customer, crediting, withdrawals that never pay twice.
On this page
Exchanges, shops and other services that hold TRAI run the wallet as a daemon. tenrai-wallet daemon keeps the keys, watches the wallet's addresses, and offers JSON-RPC over HTTP and events over WebSocket.
If you would rather sign in your own process, the SDK wallet does the same in JavaScript.
Start the daemon
tenrai-wallet init --generate-password
tenrai-wallet daemon --confirmations 3init creates the wallet of this machine and prints its 24 recovery words once; --generate-password keeps a random password beside the wallet file, so the daemon opens it unattended. The daemon listens on 127.0.0.1:28703 and writes a bearer token beside the wallet file on first start: ~/.tenrai/wallet/mainnet/wallet.vault.token on Linux. --file, --password-file and --token-file name a wallet, a password and a token in other places.
It uses the node on this machine unless --service names another, and waits for it instead of switching to the public API; a service with many addresses needs its own node.
curl http://127.0.0.1:28703/v1/rpc \
-H "Authorization: Bearer $(cat ~/.tenrai/wallet/mainnet/wallet.vault.token)" \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"new_address","params":{"label":"customer-17"}}'Amounts are strings in TRAI with up to 8 decimals, such as "1.25". Keep them as decimals or integer atoms on your side; a float cannot hold 0.1 TRAI exactly.
Deposits
| Way | How | Good for |
|---|---|---|
| An address per customer | new_address with a label, stored with the customer | Exchanges |
| One shared address | The customer puts a reference into the payment's payload | Shops, invoices |
For an invoice, hand out a payment request that carries the amount and the reference.
Credit a deposit
Subscribe to the daemon's events on ws://127.0.0.1:28703/v1/ws:
{"jsonrpc":"2.0","id":1,"method":"subscribe","params":{"events":["unconfirmed","removed","finalized","confirmations"]}}| Event | Do |
|---|---|
unconfirmed | Show the payment as incoming. Do not credit |
removed | It left the mempool without being finalized. Drop it |
finalized | The payment is final. Credit it |
confirmations | It reached --confirmations cuts. Credit here instead if your policy asks for more than one |
One finalized cut is already final on Tenrai. A higher --confirmations is house policy, not a safety requirement.
A finalized event lists its payments with transaction, output, amount, payload_text and senders. Key your ledger by transaction and output, so a payment can never be credited twice. A chat message from the messenger is a payment of one atom with messenger set to true and no payload fields.
Catch up after a restart
Events are prompts, not a ledger. Store the indexed_height of your last read and ask for everything after it whenever you start, reconnect or receive resync_required:
{"jsonrpc":"2.0","id":2,"method":"received","params":{"since_height":8812}}Withdrawals
Store a request ID of your own before you send, then pass it along:
{"jsonrpc":"2.0","id":3,"method":"send","params":{"to":"trai1…","amount":"1.25","request_id":"withdrawal-8812"}}{"jsonrpc":"2.0","id":3,"result":{"kind":"payment","to":"trai1…","amount":"1.25000000","fee":"0.00012340","fee_per_mass":"2","transaction":"…","submitted":true}}Repeating a request_id returns the first result with "duplicate": true instead of paying again. That makes the call safe to repeat after a timeout.
| Error code | Meaning | Do |
|---|---|---|
| no answer | Daemon not reachable | Repeat with the same request_id |
| -32001 | The node is down, busy or not synchronized | Repeat later |
| -32002 | The outcome of the submission is unknown. The daemon keeps sending the transaction | Repeat only with the same request_id |
| -32003 | Another operation holds the wallet | Repeat in a moment |
| -32000 | Refused, for example not enough funds | Do not repeat as is |
| -32004 | The node refused the transaction | Do not repeat as is |
| -32602 | Invalid address, amount or field | Fix the call |
The settled event, or the transaction call after a restart, tells you when a withdrawal is final. send_many pays up to seven recipients in one transaction, and "subtract_fee": true takes the fee out of the amount.
Validate a customer's withdrawal address first with validate_address. It also returns the address's verification words to show for comparison.
Many small outputs
A transaction spends at most 8 outputs. A wallet that takes many small deposits needs to merge them:
| Option | Effect |
|---|---|
--auto-compound N | Merges every address that holds more than N outputs |
"compound_first": true on send | Merges what this payment needs, then pays |
--auto-sweep-to ADDRESS | Forwards every finalized payment to a cold wallet |
Address ownership
To confirm that a customer controls a withdrawal address, send a challenge and check the signed answer:
{"jsonrpc":"2.0","id":4,"method":"proof_request","params":{"address":"trai1…"}}
{"jsonrpc":"2.0","id":5,"method":"verify_address_proof","params":{"proof":{…},"request":{…}}}The customer answers the request in the Tenrai Wallet under Settings, Ownership or with tenrai-wallet prove-address. Multisig addresses have no single key and cannot be proven this way. TCC-0004 specifies the two documents and the checks.
Security
| Topic | Rule |
|---|---|
| Token | <wallet file>.token allows every call, payments included. Only the service account may read it |
| Network | Keep the daemon on loopback. --allow-remote needs TLS in front |
| Payloads | payload_text comes from the sender. Escape it in HTML, pass it to SQL only as a parameter |
| Hot wallet | Keep only what withdrawals need; sweep the rest to a wallet that is offline |
Methods
| Group | Methods |
|---|---|
| Wallet | status (with node_synced and node_behind_by), balance, pending, rescan |
| Addresses | new_address, receive, addresses, set_label, validate_address |
| Incoming | received, incoming, outputs, history, transaction |
| Outgoing | send, send_many, sweep, burn, compound |
| Staking | validators, positions, stake, unstake, withdraw |
| Ownership | proof_request, prove_address, verify_address_proof |
| Events | subscribe, unsubscribe |
params is an object; unknown fields are refused. GET /health needs no token.
