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 | refreshWallet → GET /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-cryptoveyareact-native-quick-crypto+ Ed25519 (Web CryptoEd25519Hermes’te sınırlı olabilir —tweetnacl/@noble/ed25519+ canonical JSON’u kendiniz serileştirin).fetchile API; private keyexpo-secure-store/ Keychain.- Background’da poll:
committedolana kadar notification.
Flutter
cryptographypaketi (Ed25519) veyapinenacl.canonicalJson:jsonEncodeöncesi recursive key sort (yukarıdakinormalizegibi).flutter_secure_storageanahtar için.httpveyadio.
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#identityile imza eşleşiyor mu? - 1 UTXO, tam tutar →
transfer→committed - 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?
-
202sonrası 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ı
canonicalJson+normalizeDecimal+ test imzasıGET /api/wallet+refreshWalletplanWalletTransfer+chooseInputssubmitSignedTransaction+pollTxwaitForActiveToken+waitForTokenPeerQuorumsendPayment(merge_then_split dalı)- 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
- Web: bitkurush.org/docs
- Repo: wallet-integration.md
- Referans UI: bitkurush.org/wallet