Reference document

BitKuruş — Cüzdan istemcisi entegrasyon özeti

This is the authoritative version, rendered from the project's own source document rather than retyped — so it cannot drift from what the team maintains.

Note: Reference documents are maintained in English. A Turkish summary of this material is on the summary page.

Platform bağımsız (React Native, Flutter, Swift, Kotlin, Electron, tarayıcı).
Referans kod: public/bitkurus/wallet.js · Web doküman: /docs · Detay: docs/wallet-integration.md


1. Minimum modül listesi

Modül Görev Referans (wallet.js)
Crypto Ed25519 keygen, imza, hex signPayload, importPrivateKey
Canonical Bayt-bayt JSON canonicalJson, normalize
Money 18 ondalık string, BigInt normalizeDecimal, decimalToUnits
Api HTTPS + JSON request()
Wallet read Bakiye / UTXO refreshWalletGET /api/wallet/{pk}
Planner transfer / split / merge / merge_then_split planWalletTransfer, chooseInputs
Submit İmza + POST + poll + retry submitSignedTransaction
Sync Merge sonrası bekleme waitForActiveToken, waitForTokenPeerQuorum

Sadece imza + POST yapan bir cüzdan parçalı bakiyede ve merge sonrası ödemede hata alır.


2. API tabanı

BASE_URL = https://bitkurush.org   # veya kendi node’unuz

GET  /api/network                 # peer sayısı → quorum hesabı
GET  /api/wallet/{public_key}     # tokens[] + version
POST /api/tx/submit               # imzalı işlem
GET  /api/tx/{tx_id}              # pending | committed | rejected
GET  /api/token/{token_id}        # active mi?
GET  /api/cluster/token/{id}?expect=active   # federasyon quorum

İsteğe bağlı: GET /api/wallet/{pk}/activity?limit=30 (geçmiş UI).


3. Canonical JSON + imza (TypeScript örneği)

function normalize(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(normalize);
  if (value && typeof value === "object") {
    return Object.keys(value as object).sort().reduce((acc, k) => {
      acc[k] = normalize((value as Record<string, unknown>)[k]);
      return acc;
    }, {} as Record<string, unknown>);
  }
  return value;
}

function canonicalJson(value: unknown): string {
  return JSON.stringify(normalize(value)); // UNESCAPED_SLASHES + UNICODE
}

async function signTransactionPayload(
  payload: Omit<Transaction, "signature">,
  privateKey: CryptoKey
): Promise<string> {
  const bytes = new TextEncoder().encode(canonicalJson(payload));
  const sig = await crypto.subtle.sign({ name: "Ed25519" }, privateKey, bytes);
  return bytesToHex(new Uint8Array(sig)); // 128 lowercase hex
}

Kontrol: Tutarlar "3.000000000000000000" gibi string; 3.0 veya 3 number kullanmayın.


4. UTXO planlayıcı (port edilecek mantık)

type Token = { token_id: string; value: string; version: number };
type Plan =
  | { mode: "transfer" | "split" | "merge"; payment: TransactionDraft }
  | { mode: "merge_then_split"; merge: TransactionDraft; payment: TransactionDraft };

function planWalletTransfer(args: {
  tokens: Token[];
  amount: string;      // normalizeDecimal
  sender: string;      // 64 hex
  receiver: string;
  idFactory: (prefix: string) => string;
}): Plan {
  const selected = chooseInputs(amount, tokens); // greedy, largest first
  if (!selected) throw new Error("insufficient_balance");

  const change = selected.total - amountUnits;

  if (selected.tokens.length > 1 && change > 0n) {
    // merge_then_split: önce merge, sonra split
    const mergedId = idFactory("wallet-merged");
    return {
      mode: "merge_then_split",
      merge: { type: "merge", inputs: [...], outputs: [{ token_id: mergedId, value: total, owner: sender }] },
      payment: { type: "split", inputs: [{ token_id: mergedId, version: 1 }], outputs: [receiver, change] },
    };
  }

  const type =
    selected.tokens.length > 1 ? "merge"
    : change > 0n ? "split"
    : "transfer";

  return { mode: type, payment: { type, inputs, outputs } };
}

Şekil kuralları (sunucu reddeder):

type inputs outputs
transfer 1 1
split 1 ≥ 2
merge ≥ 2 1

5. Gönderim akışı (her işlem için)

async function submitSignedTransaction(draft: TransactionDraft, opts: WalletContext) {
  for (let attempt = 0; attempt < 4; attempt++) {
    try {
      const signature = await signTransactionPayload(draft, opts.privateKey);
      await apiPost("/api/tx/submit", { ...draft, signature }); // 202 accepted
      const final = await pollTx(draft.tx_id); // committed | rejected
      if (final.status === "rejected") throw new Error(final.reason);
      return final;
    } catch (e) {
      if (!isPeerError(e) || attempt === 3) throw e;
      await sleep(1500 * (attempt + 1));
      await refreshWallet(opts); // version güncelle
    }
  }
}

async function pollTx(txId: string, timeoutMs = 120_000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const { data } = await apiGet(`/api/tx/${txId}`);
    if (data.status === "committed" || data.status === "rejected") return data;
    await sleep(500);
  }
  throw new Error("tx_timeout");
}

isPeerError: mesajda Peer validation failed veya Insufficient peer approvals.


6. Merge sonra ödeme (kritik)

async function sendPayment(amount: string, receiver: string, ctx: WalletContext) {
  const plan = planWalletTransfer({
    tokens: ctx.tokens,
    amount,
    sender: ctx.publicKey,
    receiver,
    idFactory: newId,
  });

  if (plan.mode === "merge_then_split") {
    const mergeResult = await submitSignedTransaction(plan.merge, ctx);
    const mergedTokenId = plan.merge.outputs[0].token_id;

    await waitForActiveToken(mergedTokenId);           // GET /api/token/…
    await waitForTokenPeerQuorum(mergedTokenId, ctx);  // GET /api/cluster/token/…

    await refreshWallet(ctx);
    const merged = ctx.tokens.find(t => t.token_id === mergedTokenId)!;

    const payment = {
      ...plan.payment,
      inputs: [{ token_id: merged.token_id, version: merged.version }],
    };
    return submitSignedTransaction(payment, ctx);
  }

  return submitSignedTransaction(plan.payment, ctx);
}

Quorum:

function targetQuorum(peerCount: number): number {
  const n = peerCount + 1; // self + peers
  return n <= 1 ? 1 : Math.floor(n / 2) + 1;
}

async function waitForTokenPeerQuorum(tokenId: string, ctx: WalletContext) {
  const required = targetQuorum(ctx.network.peers.length);
  for (let i = 0; i < 40; i++) {
    const { data } = await apiGet(`/api/cluster/token/${tokenId}?expect=active`);
    if (data.confirmed_count >= Math.min(required, data.node_count)) return;
    await sleep(500);
  }
  throw new Error("peer_quorum_timeout");
}

Bu adım atlanırsa büyük transferlerde Peer validation failed görülür.


7. Platform notları

React Native / Expo

  • expo-crypto veya react-native-quick-crypto + Ed25519 (Web Crypto Ed25519 Hermes’te sınırlı olabilir — tweetnacl / @noble/ed25519 + canonical JSON’u kendiniz serileştirin).
  • fetch ile API; private key expo-secure-store / Keychain.
  • Background’da poll: committed olana kadar notification.

Flutter

  • cryptography paketi (Ed25519) veya pinenacl.
  • canonicalJson: jsonEncode öncesi recursive key sort (yukarıdaki normalize gibi).
  • flutter_secure_storage anahtar için.
  • http veya dio.

Swift / Kotlin (native)

  • CryptoKit / Tink + Ed25519; canonical JSON için sorted keys encoder yazın veya test vektörleriyle doğrulayın.
  • Keychain / EncryptedSharedPreferences.

Donanım cüzdan

  • İmza cihazda; telefon yalnızca plan + serialize + API. Canonical payload’ı cihaza hex veya hash değil, UTF-8 bytes gönderin.

8. Test kontrol listesi

  • Test vektörü: /run-a-node#identity ile imza eşleşiyor mu?
  • 1 UTXO, tam tutar → transfercommitted
  • 1 UTXO, para üstü → split → iki active output
  • 2+ UTXO, para üstü → merge → quorum → split → committed
  • Merge hemen ardından ödeme (quorum yok) → bilerek fail, sonra retry ile geçiyor mu?
  • 202 sonrası poll olmadan ikinci tx → version/lock hatası (beklenen)

9. Kullanıcıya gösterilecek hatalar

API UI metni (öneri)
Peer validation failed + reason “Ağ henüz senkron değil, birkaç saniye sonra tekrar deneyin.” + teknik detay
Insufficient peer approvals “Yeterli doğrulayıcı çevrimiçi değil.” + required/validated
Input version mismatch “Bakiye güncellendi, yeniden deneyin.”
Invalid signature “İmza hatası — geliştirici: canonical JSON kontrol edin.”

10. Tek dosyada port sırası

  1. canonicalJson + normalizeDecimal + test imzası
  2. GET /api/wallet + refreshWallet
  3. planWalletTransfer + chooseInputs
  4. submitSignedTransaction + pollTx
  5. waitForActiveToken + waitForTokenPeerQuorum
  6. sendPayment (merge_then_split dalı)
  7. Activity / UI (isteğe bağlı)

Bu sırayla giderseniz her adımda küçük entegrasyon testi yapabilirsiniz; en sık atlanan 6. adım (merge sonrası quorum).


11. Hızlı linkler