YukiChain Docs
yuki-testnet-1 · live

Build on YukiChain

A plain JSON API over HTTPS, a live block stream, ed25519 transactions you can sign in a few lines, and YUKI Connect to let people approve payments from Yuki Wallet.

Base URLhttps://chain.yukichat.lol/api
Overview

Basics

YukiChain is its own layer-1 network. Right now it runs as yuki-testnet-1 with one validator, so YUKI on it has no value: use the faucet freely.

ThingValue
Chain IDyuki-testnet-1 (part of every signature)
Units1 YUKI = 1,000,000,000 nanoYUKI. Every amount in the API is a string of nanoYUKI, so it never loses precision in JSON.
Addressesbech32 with prefix yuki: yuki1…, 20 bytes = first 20 bytes of sha256(ed25519 public key).
Keysed25519. Yuki Wallet derives them from BIP39 words (SLIP-10, path m).
FeeAt least 1000000 (0.001 YUKI) per transaction. Fees go to the block proposer.
BlocksEvery 5 s while there are pending transactions, a heartbeat block every 60 s otherwise. Final when included (single validator, no forks).
TimeAll timestamps are Unix milliseconds.
Inflation2% a year, minted to the block proposer.

Conventions

  • Every response is JSON. Errors use a non-2xx status and the body {"error": "message"}.
  • CORS is open (Access-Control-Allow-Origin: *), so you can call the API straight from a browser.
  • Anywhere an address is accepted you can also pass a name: alice or alice.yuki.
  • Lists are newest first. limit is capped per endpoint.
Reference

REST API

Press Run on any endpoint to call the live testnet from this page.

Realtime

Live stream

SSE/api/stream

A server-sent events stream. Each new block sends one data: line with the block summary and fresh chain status. A comment line : ping keeps the connection open every 25 seconds; nothing else is sent until the first block, so a browser may show the stream as “connecting” for up to a minute when the chain is idle.

const es = new EventSource('https://chain.yukichat.lol/api/stream');
es.onmessage = e => {
  const {block, status} = JSON.parse(e.data);
  console.log('block', block.height, 'with', block.txs, 'txs');
  if (block.txs > 0) refreshBalances(); // something happened
};
data: {"block":{"height":2928,"time":1793045392990,"hash":"b9d0…0f3c","txs":0,"reward":"38055861095","fees":"0","proposer":"yuki1d32n…chy6"},"status":{…same as /api/status…}}
Writing

Transactions

A transaction is a JSON object signed with the sender's ed25519 key. Build it, sign the sign bytes, and POST /api/tx. It is checked right away (signature, nonce, balance) and included in the next block.

The transaction object

FieldTypeMeaning
typestringtransfer, name_register, name_transfer or name_target
fromaddressSender; must match pubkey
pubkeyhex32-byte ed25519 public key of the sender
toaddressRecipient (transfer), new owner (name_transfer), target or "" (name_target)
amountstringnanoYUKI. Only used by transfer; "0" otherwise
feestringnanoYUKI, at least "1000000"
noncenumberThe sender's next_nonce from /api/account (counts pending txs too)
namestringName without .yuki for name_* types, else ""
memostringOptional public comment, up to 140 bytes, no newlines
sighex64-byte ed25519 signature of the sign bytes

Sign bytes

The nine fields joined with a newline, numbers in decimal, empty strings for unused fields:

chain_id \n type \n from \n to \n amount \n fee \n nonce \n name \n memo

The transaction hash is sha256(sign_bytes + "\n" + sig) as hex; the node returns it from POST /api/tx.

Example: send YUKI from JavaScript

// <script src="https://cdnjs.cloudflare.com/ajax/libs/tweetnacl/1.0.3/nacl-fast.min.js"></script>
const API = 'https://chain.yukichat.lol/api';
const hex = b => [...b].map(x => x.toString(16).padStart(2, '0')).join('');

async function send(kp, from, to, amountNano, memo = '') {
  const {chain_id} = await (await fetch(API + '/status')).json();
  const {next_nonce} = await (await fetch(API + '/account/' + from)).json();
  const tx = {type: 'transfer', from, to, amount: String(amountNano), fee: '1000000',
              nonce: next_nonce, name: '', memo, pubkey: hex(kp.publicKey)};
  const bytes = [chain_id, tx.type, tx.from, tx.to, tx.amount, tx.fee, tx.nonce, tx.name, tx.memo].join('\n');
  tx.sig = hex(nacl.sign.detached(new TextEncoder().encode(bytes), kp.secretKey));
  const r = await fetch(API + '/tx', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(tx)});
  const j = await r.json();
  if (!r.ok) throw new Error(j.error);
  return j.hash;
}

Types

typeWhat it doesCost to sender
transferMoves amount to to.amount + fee
name_registerRegisters name for one year, or renews it by a year if you already own it.name price (burned) + fee
name_transferGives the name to to and clears its target.fee
name_targetPoints the name at another address (to), or back at the owner with "".fee

Name prices

Names are 3 to 32 characters: a-z, 0-9 and single dashes, not starting or ending with a dash. The yearly price depends on length and is burned. A name with any digit costs half.

Length3456789–1112+
YUKI / year3000100030010050251510

Common errors from POST /api/tx

errorFix
bad nonce: want NUse next_nonce from /api/account; send one transaction at a time or count up yourself.
bad signatureSign bytes differ: check the chain ID, field order and that amounts are decimal strings.
pubkey does not match senderfrom must be the address of pubkey.
insufficient balanceBalance must cover this and all your pending transactions.
fee below minimum 1000000Pay at least 0.001 YUKI.
memo too long or has newlinesUp to 140 bytes on one line.
For dApps

YUKI Connect

YUKI Connect lets a website talk to Yuki Wallet: the person scans a QR code (or taps a link on their phone), approves the connection, and from then on your site can see their address and ask them to approve payments or sign messages. Keys never leave the wallet.

Try it

Connect your Yuki Wallet to this page, then send yourself some test YUKI or sign a message. Have no wallet yet? Get one and tap Faucet.

Live demo

Not connected

Account
StatusConnect a wallet first
Send a payment
Sign a message

Quick start

  1. Host a manifest at a public HTTPS URL, with CORS allowed. The wallet shows its name and icon, and the domain of url is what proofs and signatures are bound to.
    {
      "url": "https://your.app",
      "name": "Your App",
      "iconUrl": "https://your.app/icon-180.png"
    }
  2. Load the SDK (it pulls in tweetnacl and the QR encoder from cdnjs by itself):
    <script src="https://chain.yukichat.lol/sdk/yuki-connect.js"></script>
  3. Add the button and react to the account:
    const yc = new YukiConnect({manifestUrl: 'https://your.app/yuki-connect.json'});
    yc.button('#connect');                 // Connect button, becomes an account chip with a menu
    yc.onStatusChange(account => {
      if (account) console.log('connected', account.address, account.name);
      else console.log('disconnected');
    });
  4. Ask for a payment. The person sees a compact approval window in the wallet and holds a button to confirm:
    const {hash} = await yc.sendTransaction({
      to: 'yuki1…',                        // or resolve a name with /api/account/alice.yuki first
      amount: YukiConnect.toNano('2.5'),   // string of nanoYUKI
      memo: 'Order #1042',
    });

SDK reference

CallWhat it does
new YukiConnect(opts)manifestUrl (required), theme: 'auto' | 'dark' | 'light', storageKey (default 'yuki-connect'), bridge, walletUrl. The session survives reloads (localStorage).
yc.connect({proof?})Opens the connect window (QR + “Open Yuki Wallet”). Resolves with the account, rejects with code: 300 if declined or 'closed' if the window is closed. Pass proof (a server nonce) to get a signed ownership proof.
yc.button(el, {proof?})Renders the Connect button / account chip into an element and keeps it in sync. proof may be a function.
yc.account{address, pubkey, name, chain, device, proof} or null. name is the wallet's active .yuki name, if any.
yc.onStatusChange(fn)Called with the account on connect and null on disconnect (from either side). Returns an unsubscribe function.
yc.sendTransaction(tx, {modal?}){to, amount, memo?, valid_until?} → {hash}. Shows a “Confirm in Yuki Wallet” window unless modal: false. Default expiry 5 minutes.
yc.signMessage(text)→ {text, signature, pubkey, address, domain, timestamp}. Check it with YukiConnect.verifySignedMessage.
yc.disconnect()Tells the wallet and forgets the session.
yc.connectLink(request)The raw universal link, if you want to draw your own UI.
YukiConnect.toNano / fromNano'1.5' ↔ '1500000000'
YukiConnect.verifyProof(account)Async. True if the wallet key matches the address and signed your proof payload.

Errors carry a code: 300 declined by the person, 400 bad request, 500 the wallet could not send (the message says why), 'expired', 'cancelled' (closed the window on your side).

Log in with a proof

An address alone proves nothing: anyone can type one. To log someone in, give the wallet a one-time nonce from your server. The wallet signs it together with your domain, and your server checks the signature.

// browser
const nonce = await (await fetch('/auth/nonce')).text();
const account = await yc.connect({proof: nonce});
await fetch('/auth/verify', {method: 'POST', body: JSON.stringify(account)});

// server (Node, tweetnacl): rebuild the signed string exactly
const msg = ['yuki-connect/v1', p.domain, p.timestamp, account.address, p.payload].join('\n');
ok = p.domain === 'your.app' && p.payload === issuedNonce && Date.now() - p.timestamp < 5 * 60e3
  && nacl.sign.detached.verify(Buffer.from(msg), Buffer.from(p.signature, 'hex'), Buffer.from(account.pubkey, 'hex'))
  && addressOf(account.pubkey) === account.address;   // bech32('yuki', sha256(pubkey)[0:20])

signMessage works the same way with the prefix yuki-signed-message/v1 and text in place of payload.

Protocol

You only need this to write your own SDK or wallet.

  1. Session keys. The dApp makes an X25519 key pair (nacl.box.keyPair()). Its public key, as 64 hex chars, is the dApp's client ID.
  2. Connect link. https://wallet.yukichat.lol/connect?v=1&id=<dApp client ID>&r=<base64url(JSON request)>, where the request is {"manifestUrl": "…", "items": [{"name": "address"}, {"name": "proof", "payload": "…"}]}. The Android app opens these links directly; anywhere else they open the web wallet.
  3. Wallet session. On approval the wallet makes its own X25519 key pair and sends a connect event to the dApp's client ID through the bridge. From then on both sides only accept messages from each other's client ID.
  4. Encryption. Each message is JSON, encrypted with nacl.box(json, nonce, theirPublic, mySecret). On the wire: base64(nonce[24] ‖ box).
DirectionMessage
wallet → dApp{"event":"connect","payload":{"items":[{"name":"address","address","pubkey","chain","yukiName"},{"name":"proof","proof":{"timestamp","domain","payload","signature"}}],"device":{"app","version","platform"}}}
wallet → dApp{"event":"connect_error","payload":{"code":300,"message":"Declined in wallet"}}
dApp → wallet{"method":"sendTransaction","id":"1","params":{"to","amount","memo","valid_until"}}
dApp → wallet{"method":"signMessage","id":"2","params":{"text"}}
wallet → dApp{"id":"1","result":{"hash"}} or {"id":"1","error":{"code","message"}}
either way{"method":"disconnect"} (dApp) / {"event":"disconnect"} (wallet)

Bridge API

The bridge is a dumb relay at https://chain.yukichat.lol/bridge. It keeps each message for its TTL (max 300 s) so a wallet that opens a minute later still gets it.

POST/bridge/message?client_id={from}&to={to}&ttl={seconds}

Body: the base64 encrypted message as plain text, up to 64 KB. Returns {"ok":true,"id":42}.

SSE/bridge/events?client_id={id1,id2,…}&last_event_id={n}

Streams event: message with data: {"from":"…","message":"…"} and an id you pass back as last_event_id after a reconnect. Up to 100 client IDs per connection; event: heartbeat every 15 s.