// Native-JS arkoor send: checkpoint + arkoor transaction construction, // BIP-341 sighashes, MuSig2 cosign ceremony with the ASP, and signed-VTXO // assembly. Mirrors bark's lib/src/arkoor/mod.rs ArkoorBuilder (checkpoint // mode, no dust isolation). import { hex } from '@scure/base'; import { sha256 } from '@noble/hashes/sha256'; import { ripemd160 } from '@noble/hashes/ripemd160'; import { schnorr, secp256k1 } from '@noble/curves/secp256k1'; import * as musig2 from '@scure/btc-signer/musig2'; import { concatBytes, reader, grpcCall, pbWriter, pbFields } from './proto.js'; // hex string or byte array -> bytes (decoded vtxos carry hex-string fields) const asBytes = (v) => (typeof v === 'string' ? hex.decode(v) : Uint8Array.from(v)); // The key that cosigns arkoor spends of a vtxo with this policy (bark's // VtxoPolicy::arkoor_pubkey). export const policyOwnerPubkey = (policy) => policy.userPubkey; const te = new TextEncoder(); // --------------------------------------------------------------------------- // small bitcoin helpers // --------------------------------------------------------------------------- const u16le = (n) => Uint8Array.of(n & 0xff, (n >> 8) & 0xff); const u32le = (n) => Uint8Array.of(n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >>> 24) & 0xff); const u64le = (n) => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(n), true); return b; }; const varint = (n) => { if (n < 0xfd) return Uint8Array.of(n); if (n <= 0xffff) return concatBytes(Uint8Array.of(0xfd), u16le(n)); return concatBytes(Uint8Array.of(0xfe), u32le(n)); }; const sha256d = (b) => sha256(sha256(b)); const taggedHash = (tag, ...data) => { const th = sha256(te.encode(tag)); return sha256(concatBytes(th, th, ...data)); }; // minimal script-number push (as bitcoin's push_int) function pushInt(n) { if (n === 0) return Uint8Array.of(0x00); // OP_0 if (n >= 1 && n <= 16) return Uint8Array.of(0x50 + n); // OP_1..OP_16 const bytes = []; let v = n; while (v > 0) { bytes.push(v & 0xff); v >>= 8; } if (bytes[bytes.length - 1] & 0x80) bytes.push(0x00); return Uint8Array.of(bytes.length, ...bytes); } const OP = { CSV: 0xb2, CLTV: 0xb1, DROP: 0x75, CHECKSIG: 0xac, HASH160: 0xa9, EQUALVERIFY: 0x88, SIZE: 0x82 }; export const P2A_SCRIPT = hex.decode('51024e73'); // delayed_sign: OP_CSV OP_DROP OP_CHECKSIG const delayedSignScript = (delta, xonly) => concatBytes(pushInt(delta), Uint8Array.of(OP.CSV, OP.DROP, 0x20), xonly, Uint8Array.of(OP.CHECKSIG)); // timelock_sign: OP_CLTV OP_DROP OP_CHECKSIG const timelockSignScript = (height, xonly) => concatBytes(pushInt(height), Uint8Array.of(OP.CLTV, OP.DROP, 0x20), xonly, Uint8Array.of(OP.CHECKSIG)); // hash_delay_sign_v0: OP_CSV OP_DROP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG // (the witness reveals the 32B preimage; HASH160(preimage) = ripemd160(sha256(preimage))) const hashDelaySignScript = (paymentHash, delta, xonly) => concatBytes( pushInt(delta), Uint8Array.of(OP.CSV, OP.DROP, OP.HASH160, 0x14), ripemd160(paymentHash), Uint8Array.of(OP.EQUALVERIFY, 0x20), xonly, Uint8Array.of(OP.CHECKSIG)); // hash_delay_sign (v1, pver 5): same, but the preimage's size is checked to be // 32 bytes before hashing — OP_SIZE <32> OP_EQUALVERIFY inserted after the // timelock, mirroring Lightning's own on-chain check. const hashDelaySignScriptV1 = (paymentHash, delta, xonly) => concatBytes( pushInt(delta), Uint8Array.of(OP.CSV, OP.DROP, OP.SIZE, 0x01, 0x20, OP.EQUALVERIFY, OP.HASH160, 0x14), ripemd160(paymentHash), Uint8Array.of(OP.EQUALVERIFY, 0x20), xonly, Uint8Array.of(OP.CHECKSIG)); // delay_timelock_sign: OP_CLTV OP_DROP OP_CSV OP_DROP OP_CHECKSIG const delayTimelockSignScript = (height, delta, xonly) => concatBytes( pushInt(height), Uint8Array.of(OP.CLTV, OP.DROP), pushInt(delta), Uint8Array.of(OP.CSV, OP.DROP, 0x20), xonly, Uint8Array.of(OP.CHECKSIG)); // --------------------------------------------------------------------------- // taproot + musig key aggregation // --------------------------------------------------------------------------- const xonly = (pub33) => pub33.slice(1); // bark: musig::combine_keys — KeySort then KeyAgg, no tweak export function musigInternalKey(keys) { const sorted = musig2.sortKeys(keys.map((k) => Uint8Array.from(k))); const ctx = musig2.keyAggregate(sorted); return { sortedKeys: sorted, internalXOnly: musig2.keyAggExport(ctx) }; // 32B x-only } const tapLeafHash = (script) => taggedHash('TapLeaf', Uint8Array.of(0xc0), varint(script.length), script); // BIP-341 branch hash: children sorted lexicographically const tapBranch = (a, b) => { let swap = false; for (let i = 0; i < 32; i++) { if (a[i] !== b[i]) { swap = a[i] > b[i]; break; } } return swap ? taggedHash('TapBranch', b, a) : taggedHash('TapBranch', a, b); }; // Taproot around a musig2(user,server) internal key with 1 or 2 script leaves // (two leaves sit at depth 1, mirroring bark's TaprootBuilder usage). // Returns everything both tx-building and signing need. export function taprootFromLeaves(userPub, serverPub, leafScripts) { const { sortedKeys, internalXOnly } = musigInternalKey([userPub, serverPub]); const leafHashes = leafScripts.map(tapLeafHash); const merkleRoot = leafHashes.length === 1 ? leafHashes[0] : tapBranch(leafHashes[0], leafHashes[1]); const tapTweak = taggedHash('TapTweak', internalXOnly, merkleRoot); const P = secp256k1.ProjectivePoint; // lift internal x-only, add tweak*G const internalPoint = P.fromHex(concatBytes(Uint8Array.of(0x02), internalXOnly)); const outputPoint = internalPoint.add(P.BASE.multiply(BigInt('0x' + hex.encode(tapTweak)))); const outputCompressed = outputPoint.toRawBytes(true); const outputXOnly = outputCompressed.slice(1); const scriptPubKey = concatBytes(hex.decode('5120'), outputXOnly); const outputParity = outputCompressed[0] === 0x03 ? 1 : 0; // for script-path control blocks return { sortedKeys, internalXOnly, merkleRoot, tapTweak, outputXOnly, outputParity, scriptPubKey }; } export const taprootOneLeaf = (userPub, serverPub, leafScript) => taprootFromLeaves(userPub, serverPub, [leafScript]); // PubkeyVtxoPolicy: keyspend musig(user,server); leaf = delayed_sign(exit_delta, user). // leafScript is returned so a unilateral exit can claim through it after the CSV. export const pubkeyPolicyTaproot = (userPub, serverPub, exitDelta) => { const leafScript = delayedSignScript(exitDelta, xonly(userPub)); return { ...taprootOneLeaf(userPub, serverPub, leafScript), leafScript }; }; // CheckpointVtxoPolicy: keyspend musig(user,server); leaf = timelock_sign(expiry, server) export const checkpointPolicyTaproot = (userPub, serverPub, expiryHeight) => taprootOneLeaf(userPub, serverPub, timelockSignScript(expiryHeight, xonly(serverPub))); // Taproot for any user-facing VTXO policy (mirror of bark's VtxoPolicy::taproot). // policy fields may be hex strings (decoded form) or byte arrays. export function policyTaproot(policy, serverPub, exitDelta) { const user = asBytes(policy.userPubkey); if (policy.type === 'pubkey') return pubkeyPolicyTaproot(user, serverPub, exitDelta); // The bare types are the 0.6.0 (pver 5) policies whose hashlock leaves // check the preimage size; _v0 keeps building the legacy scripts so vtxos // minted before the upgrade still reconstruct, sign, and exit correctly. if (policy.type === 'serverHtlcSend' || policy.type === 'serverHtlcSend_v0') { const hashDelay = policy.type === 'serverHtlcSend' ? hashDelaySignScriptV1 : hashDelaySignScript; const hash = asBytes(policy.paymentHash); // leaf 1: server spends with preimage after exit_delta // leaf 2: user refund after htlc expiry + 2*exit_delta delay const serverClaim = hashDelay(hash, exitDelta, xonly(serverPub)); const userRefund = delayTimelockSignScript(policy.htlcExpiry, 2 * exitDelta, xonly(user)); return { ...taprootFromLeaves(user, serverPub, [serverClaim, userRefund]), serverClaim, userRefund }; } if (policy.type === 'serverHtlcRecv' || policy.type === 'serverHtlcRecv_v0') { const hashDelay = policy.type === 'serverHtlcRecv' ? hashDelaySignScriptV1 : hashDelaySignScript; const hash = asBytes(policy.paymentHash); // leaf 1: server reclaim after htlc expiry + exit_delta delay // leaf 2: user spends with preimage after htlc_expiry_delta + exit_delta const serverRefund = delayTimelockSignScript(policy.htlcExpiry, exitDelta, xonly(serverPub)); const userClaim = hashDelay(hash, policy.htlcExpiryDelta + exitDelta, xonly(user)); return { ...taprootFromLeaves(user, serverPub, [serverRefund, userClaim]), serverRefund, userClaim }; } throw new Error('unsupported vtxo policy type ' + policy.type); } // --------------------------------------------------------------------------- // transactions (version 3, zero locktime, sequence 0) // --------------------------------------------------------------------------- const serializeTx = (tx) => concatBytes( u32le(tx.version), varint(tx.inputs.length), ...tx.inputs.flatMap((i) => [i.prevout, Uint8Array.of(0x00), u32le(i.sequence)]), varint(tx.outputs.length), ...tx.outputs.flatMap((o) => [u64le(o.valueSat), varint(o.scriptPubKey.length), o.scriptPubKey]), u32le(tx.locktime), ); export const txid = (tx) => sha256d(serializeTx(tx)); // internal byte order const outpointBytes = (txidInternal, vout) => concatBytes(txidInternal, u32le(vout)); // BIP-341 key-spend sighash, SIGHASH_DEFAULT, single input export function taprootSighash(tx, prevouts) { const shaPrevouts = sha256(concatBytes(...tx.inputs.map((i) => i.prevout))); const shaAmounts = sha256(concatBytes(...prevouts.map((p) => u64le(p.valueSat)))); const shaScripts = sha256(concatBytes(...prevouts.map((p) => concatBytes(varint(p.scriptPubKey.length), p.scriptPubKey)))); const shaSequences = sha256(concatBytes(...tx.inputs.map((i) => u32le(i.sequence)))); const shaOutputs = sha256(concatBytes(...tx.outputs.map((o) => concatBytes(u64le(o.valueSat), varint(o.scriptPubKey.length), o.scriptPubKey)))); return taggedHash('TapSighash', Uint8Array.of(0x00), // sighash epoch Uint8Array.of(0x00), // hash type: default u32le(tx.version), u32le(tx.locktime), shaPrevouts, shaAmounts, shaScripts, shaSequences, shaOutputs, Uint8Array.of(0x00), // spend type: key path, no annex u32le(0), // input index ); } // --------------------------------------------------------------------------- // VTXO encoding (mirror of decodeVtxo) // --------------------------------------------------------------------------- const encodePubkeyPolicy = (pub33) => concatBytes(Uint8Array.of(0x00), pub33); // Serialize any user-facing VtxoPolicy (mirror of proto.js decodePolicy). export function encodePolicy(p) { const user = asBytes(p.userPubkey); if (p.type === 'pubkey') return encodePubkeyPolicy(user); if (p.type === 'serverHtlcSend') { return concatBytes(Uint8Array.of(0x09), user, asBytes(p.paymentHash), u32le(p.htlcExpiry)); } if (p.type === 'serverHtlcRecv') { return concatBytes(Uint8Array.of(0x08), user, asBytes(p.paymentHash), u32le(p.htlcExpiry), u16le(p.htlcExpiryDelta)); } if (p.type === 'serverHtlcSend_v0') { return concatBytes(Uint8Array.of(0x01), user, asBytes(p.paymentHash), u32le(p.htlcExpiry)); } if (p.type === 'serverHtlcRecv_v0') { return concatBytes(Uint8Array.of(0x02), user, asBytes(p.paymentHash), u32le(p.htlcExpiry), u16le(p.htlcExpiryDelta)); } throw new Error('unsupported vtxo policy type ' + p.type); } // GenesisTransition::Arkoor const encodeArkoorTransition = ({ cosigners, tapTweak, signature }) => concatBytes( Uint8Array.of(0x02), varint(cosigners.length), ...cosigners, tapTweak, signature ?? new Uint8Array(64), ); const encodeGenesisItem = ({ transition, nbOutputs, outputIdx, otherOutputs, feeSat }) => concatBytes( transition, Uint8Array.of(nbOutputs, outputIdx), ...otherOutputs.map((o) => concatBytes(u64le(o.valueSat), varint(o.scriptPubKey.length), o.scriptPubKey)), u64le(feeSat), ); // Assemble a Vtxo from the spent input's raw genesis bytes + new items. function encodeVtxo({ amountSat, expiryHeight, serverPubkey, exitDelta, anchorPointRaw, inputGenesisRaw, inputGenesisCount, newItems, policyBytes, pointRaw }) { return concatBytes( u16le(2), // encoding version u64le(amountSat), u32le(expiryHeight), serverPubkey, u16le(exitDelta), anchorPointRaw, varint(inputGenesisCount + newItems.length), inputGenesisRaw, ...newItems, policyBytes, pointRaw, ); } // --------------------------------------------------------------------------- // the send itself // --------------------------------------------------------------------------- const DUST_SAT = 330; // P2TR dust threshold // Mirror of bark's ArkoorBuilder::new_isolate_dust: the server rejects an // arkoor whose outputs mix dust and non-dust, so dust outputs are routed // through a combined isolation output (spent by a fanout tx). When the dust // alone can't reach the threshold, a non-dust output is split to pad it. function isolateDust(outputs) { const sum = (l) => l.reduce((n, o) => n + o.amountSat, 0); const dust = outputs.filter((o) => o.amountSat < DUST_SAT); const nonDust = outputs.filter((o) => o.amountSat >= DUST_SAT); if (!dust.length || !nonDust.length) return { outs: outputs, isolated: [] }; if (sum(dust) >= DUST_SAT) return { outs: nonDust, isolated: dust }; // if breaking a vtxo would just create more dust, accept the mix if (sum(nonDust) < 2 * DUST_SAT) return { outs: outputs, isolated: [] }; const deficit = DUST_SAT - sum(dust); const idx = nonDust.findIndex((o) => o.amountSat - deficit >= DUST_SAT); if (idx < 0) return { outs: [...nonDust, ...dust], isolated: [] }; const outs = nonDust.slice(); outs[idx] = { amountSat: outs[idx].amountSat - deficit, policy: outs[idx].policy }; return { outs, isolated: [{ amountSat: deficit, policy: nonDust[idx].policy }, ...dust] }; } // Build everything for a checkpointed single-input arkoor send. // input: decoded vtxo (from decodeVtxo, with _raw), owned by keys.vtxo // outputs: [{ amountSat, userPubkey (33B) }...] or [{ amountSat, policy }...] // — destination first, change last. NB an output may come back SPLIT across // two result vtxos (dust padding), so callers should consume // buildAllSignedVtxos() and classify by policy rather than by index. export function buildArkoorSend({ input, outputs, serverPubkey, vtxoKeys }) { const userPub = vtxoKeys.pubkey; if (hex.encode(userPub) !== policyOwnerPubkey(input.policy)) throw new Error('input not owned by our key'); // normalize plain-pubkey outputs to policy form, then split off dust const normalized = outputs.map((o) => ({ amountSat: o.amountSat, policy: o.policy || { type: 'pubkey', userPubkey: hex.encode(o.userPubkey) }, })); const { outs, isolated } = isolateDust(normalized); const isolationSat = isolated.reduce((n, o) => n + o.amountSat, 0); const inputTaproot = policyTaproot(input.policy, serverPubkey, input.exitDelta); const checkpointTaproot = checkpointPolicyTaproot(userPub, serverPubkey, input.expiryHeight); // checkpoint tx: spends the input vtxo, one output per destination plus the // combined dust isolation output (all with the checkpoint policy spk) + P2A const checkpointTx = { version: 3, locktime: 0, inputs: [{ prevout: input.point.raw, sequence: 0 }], outputs: [ ...outs.map((o) => ({ valueSat: o.amountSat, scriptPubKey: checkpointTaproot.scriptPubKey })), ...(isolated.length ? [{ valueSat: isolationSat, scriptPubKey: checkpointTaproot.scriptPubKey }] : []), { valueSat: 0, scriptPubKey: P2A_SCRIPT }, ], }; const checkpointTxid = txid(checkpointTx); // one arkoor tx per non-dust output, spending checkpoint:vout const arkoorTxs = outs.map((o, vout) => { const destTaproot = policyTaproot(o.policy, serverPubkey, input.exitDelta); return { version: 3, locktime: 0, inputs: [{ prevout: outpointBytes(checkpointTxid, vout), sequence: 0 }], outputs: [ { valueSat: o.amountSat, scriptPubKey: destTaproot.scriptPubKey }, { valueSat: 0, scriptPubKey: P2A_SCRIPT }, ], }; }); // fanout tx: spends the isolation output, one output per isolated dest const fanoutTx = isolated.length ? { version: 3, locktime: 0, inputs: [{ prevout: outpointBytes(checkpointTxid, outs.length), sequence: 0 }], outputs: [ ...isolated.map((o) => ({ valueSat: o.amountSat, scriptPubKey: policyTaproot(o.policy, serverPubkey, input.exitDelta).scriptPubKey, })), { valueSat: 0, scriptPubKey: P2A_SCRIPT }, ], } : null; // sighashes: [checkpoint spend of input, arkoor_i of checkpoint:i, fanout?] const inputPrevout = { valueSat: input.amountSat, scriptPubKey: inputTaproot.scriptPubKey }; const sighashes = [ taprootSighash(checkpointTx, [inputPrevout]), ...arkoorTxs.map((tx, i) => taprootSighash(tx, [checkpointTx.outputs[i]])), ...(fanoutTx ? [taprootSighash(fanoutTx, [checkpointTx.outputs[outs.length]])] : []), ]; // taptweaks per signature (bark: taptweak_at) — everything after the input // spends a checkpoint-policy output const tweaks = [inputTaproot.tapTweak, ...sighashes.slice(1).map(() => checkpointTaproot.tapTweak)]; return { outputs: outs, isolated, checkpointTx, checkpointTxid, arkoorTxs, fanoutTx, sighashes, tweaks, inputTaproot, checkpointTaproot, }; } // "arkoor cosign attestation " — 32-byte prefix const ATTESTATION_PREFIX = te.encode('arkoor cosign attestation '); // outputs here = regular outputs followed by isolated outputs (bark's // all_outputs() order) export function cosignAttestation(inputVtxoIdRaw, outputs, vtxoPrivkey) { const msg = sha256(concatBytes( ATTESTATION_PREFIX, inputVtxoIdRaw, u32le(outputs.length), ...outputs.flatMap((o) => [u64le(o.amountSat), encodePolicy(o.policy)]), )); return schnorr.sign(msg, vtxoPrivkey); } // One musig2 nonce per sighash, bound to it. export const genUserNonces = (build, vtxoKeys) => build.sighashes.map((sh) => musig2.nonceGen(vtxoKeys.pubkey, vtxoKeys.privkey, undefined, sh)); // Serialize one ArkoorCosignRequest part for a built arkoor spend. // `preimage` (32B) is required when the input carries an HTLC policy and the // claimer is claiming it; omitting it restricts the spend to a full refund. export function cosignPartBytes({ build, input, vtxoKeys, nonces, preimage }) { const destBytes = (o) => { const dest = pbWriter(); dest.varintField(1, o.amountSat); dest.bytesField(2, encodePolicy(o.policy)); return dest.finish(); }; const part = pbWriter(); part.bytesField(1, input.point.raw); // vtxo id = outpoint bytes for (const o of build.outputs) part.bytesField(2, destBytes(o)); for (const n of nonces) part.bytesField(3, n.public); for (const o of build.isolated) part.bytesField(4, destBytes(o)); // dust isolation part.varintField(5, 1); // use_checkpoint = true part.bytesField(6, cosignAttestation( input.point.raw, [...build.outputs, ...build.isolated], vtxoKeys.privkey)); if (preimage) part.bytesField(7, asBytes(preimage)); return part.finish(); } // Parse an ArkoorPackageCosignResponse into per-part nonce/partial lists. export function parsePackageCosignResponse(respBytes) { const parts = []; for (const { field, value } of pbFields(respBytes)) { if (field !== 1) continue; const p = { serverNonces: [], serverPartials: [] }; for (const f of pbFields(value)) { if (f.field === 1) p.serverNonces.push(f.value); if (f.field === 2) p.serverPartials.push(f.value); } parts.push(p); } return parts; } // Combine our partials with the server's into final schnorr signatures, // verifying the server partial and the combined signature along the way. export function combineCosign({ build, nonces, serverResp, vtxoKeys, serverPubkey }) { const nSigs = build.sighashes.length; const { serverNonces, serverPartials } = serverResp; if (serverNonces.length !== nSigs || serverPartials.length !== nSigs) { throw new Error(`bad cosign response: ${serverNonces.length} nonces, ${serverPartials.length} partials, wanted ${nSigs}`); } const finalSigs = []; for (let i = 0; i < nSigs; i++) { const aggNonce = musig2.nonceAggregate([nonces[i].public, serverNonces[i]]); const session = new musig2.Session( aggNonce, build.inputTaproot.sortedKeys, build.sighashes[i], [build.tweaks[i]], [true], ); // verify the server's partial before combining (bark does the same) const serverIdx = build.inputTaproot.sortedKeys.findIndex( (k) => hex.encode(k) === hex.encode(serverPubkey)); const noncesInKeyOrder = serverIdx === 0 ? [serverNonces[i], nonces[i].public] : [nonces[i].public, serverNonces[i]]; if (!session.partialSigVerify(serverPartials[i], noncesInKeyOrder, serverIdx)) { throw new Error(`server partial signature ${i} is invalid`); } const userPartial = session.sign(nonces[i].secret, vtxoKeys.privkey); const finalSig = session.partialSigAgg([userPartial, serverPartials[i]]); // self-check against the taproot output key this signature must satisfy const outputKey = i === 0 ? build.inputTaproot.outputXOnly : build.checkpointTaproot.outputXOnly; if (!schnorr.verify(finalSig, build.sighashes[i], outputKey)) { throw new Error(`combined signature ${i} does not verify against taproot output key`); } finalSigs.push(finalSig); } return finalSigs; } // full MuSig2 ceremony over the ASP's RequestArkoorCosign export async function cosignWithServer(ark, build, { input, vtxoKeys, serverPubkey, preimage }) { const [sigs] = await cosignPackageWithServer(ark, [{ build, input, vtxoKeys, preimage }], serverPubkey); return sigs; } // Same ceremony for a PACKAGE of arkoor parts (one per input vtxo) in a single // atomic request — the server cosigns all or none, so a multi-input send can't // half-spend. Returns one finalSigs array per part, in order. export async function cosignPackageWithServer(ark, parts, serverPubkey) { const noncesList = parts.map((p) => genUserNonces(p.build, p.vtxoKeys)); const req = pbWriter(); parts.forEach((p, i) => req.bytesField(1, cosignPartBytes({ build: p.build, input: p.input, vtxoKeys: p.vtxoKeys, nonces: noncesList[i], preimage: p.preimage, }))); const respBytes = await grpcCall(ark, 'bark_server.ArkService/RequestArkoorCosign', req.finish()); const resps = parsePackageCosignResponse(respBytes); if (resps.length !== parts.length) throw new Error('bad cosign response count'); return parts.map((p, i) => combineCosign({ build: p.build, nonces: noncesList[i], serverResp: resps[i], vtxoKeys: p.vtxoKeys, serverPubkey, })); } // assemble the final signed Vtxo bytes for output `idx`, where idx // indexes [regular outputs..., isolated outputs...] export function buildSignedVtxoBytes({ input, build, finalSigs, serverPubkey, idx }) { const isP2a = (out) => hex.encode(out.scriptPubKey) === hex.encode(P2A_SCRIPT); const cosigners = [Uint8Array.from(hex.decode(policyOwnerPubkey(input.policy)))]; const raw = input._raw; const finish = (o, items, pointRaw) => encodeVtxo({ amountSat: o.amountSat, expiryHeight: input.expiryHeight, serverPubkey, exitDelta: input.exitDelta, anchorPointRaw: input.anchorPoint.raw, inputGenesisRaw: raw.bytes.slice(raw.itemsStart, raw.itemsEnd), inputGenesisCount: raw.nItems, newItems: items, policyBytes: encodePolicy(o.policy), pointRaw, }); // NB nb_outputs on the wire counts own output + other_outputs — the P2A // fee anchor is excluded (bark: other_outputs.len() + 1). if (idx < build.outputs.length) { // regular output: input -> checkpoint (own vout idx) -> arkoor const o = build.outputs[idx]; const checkpointOthers = build.checkpointTx.outputs.filter((out, i) => i !== idx && !isP2a(out)); const checkpointItem = encodeGenesisItem({ transition: encodeArkoorTransition({ cosigners, tapTweak: build.inputTaproot.tapTweak, signature: finalSigs[0], }), nbOutputs: checkpointOthers.length + 1, outputIdx: idx, otherOutputs: checkpointOthers, feeSat: 0, }); const arkoorItem = encodeGenesisItem({ transition: encodeArkoorTransition({ cosigners, tapTweak: build.checkpointTaproot.tapTweak, signature: finalSigs[1 + idx], }), nbOutputs: 1, outputIdx: 0, otherOutputs: [], feeSat: 0, }); return finish(o, [checkpointItem, arkoorItem], outpointBytes(txid(build.arkoorTxs[idx]), 0)); } // isolated output: input -> checkpoint (isolation vout) -> fanout (own vout) const j = idx - build.outputs.length; const o = build.isolated[j]; const isolationIdx = build.outputs.length; const fanoutSigIdx = 1 + build.outputs.length; const checkpointOthers = build.checkpointTx.outputs.filter((out, i) => i !== isolationIdx && !isP2a(out)); const checkpointItem = encodeGenesisItem({ transition: encodeArkoorTransition({ cosigners, tapTweak: build.inputTaproot.tapTweak, signature: finalSigs[0], }), nbOutputs: checkpointOthers.length + 1, outputIdx: isolationIdx, otherOutputs: checkpointOthers, feeSat: 0, }); const fanoutOthers = build.fanoutTx.outputs.filter((out, i) => i !== j && !isP2a(out)); const fanoutItem = encodeGenesisItem({ transition: encodeArkoorTransition({ cosigners, tapTweak: build.checkpointTaproot.tapTweak, signature: finalSigs[fanoutSigIdx], }), nbOutputs: fanoutOthers.length + 1, outputIdx: j, otherOutputs: fanoutOthers, feeSat: 0, }); return finish(o, [checkpointItem, fanoutItem], outpointBytes(txid(build.fanoutTx), j)); } // All result vtxos of an arkoor build (regular then isolated), signed. export function buildAllSignedVtxos({ input, build, finalSigs, serverPubkey }) { const n = build.outputs.length + build.isolated.length; return Array.from({ length: n }, (_, idx) => buildSignedVtxoBytes({ input, build, finalSigs, serverPubkey, idx })); } // --------------------------------------------------------------------------- // remaining RPCs // --------------------------------------------------------------------------- export async function registerVtxoTransactions(ark, vtxoBytesList) { const w = pbWriter(); for (const v of vtxoBytesList) w.bytesField(1, v); await grpcCall(ark, 'bark_server.ArkService/RegisterVtxoTransactions', w.finish()); } export async function postArkoorMessage(ark, blindedId, vtxoBytesList) { const w = pbWriter(); w.bytesField(1, blindedId); for (const v of vtxoBytesList) w.bytesField(2, v); await grpcCall(ark, 'mailbox_server.MailboxService/PostArkoorMessage', w.finish()); } pan class="p">} }, "error": { "header": "Ошибка", "message": "Пожалуйста, попробуйте еще раз или обратитесь в службу поддержки.", "signIn": "Пожалуйста, войдите, чтобы продолжить.", "unrecognizedInput": "Нераспознанный ввод" }, "faq": { "header": "Часто задаваемые вопросы", "cost": { "question": "Сколько это стоит?", "answer": "Поступление платежей на свой счет бесплатное. При выводе средств на внешние кошельки взимается небольшая комиссия, которая варьируется в зависимости от условий сети." }, "compatibility": { "question": "Нужно ли мне какое-либо специальное устройство или программное обеспечение?", "answer": "Нет, Coinos работает везде, где есть Интернет. Продавцы могут использовать его на планшете или разместить QR-код, чтобы покупатели могли сканировать его с помощью телефона." }, "safety": { "question": "Когда мои средства окупятся?", "answer": "Платежи Lightning производятся мгновенно. Вы можете вывести свой баланс на другой кошелек за считанные секунды. Подтверждение депозита в сети занимает в среднем 5-10 минут." }, "pos": { "question": "Может ли Coinos интегрироваться с моим существующим PoS?", "answer": "Мы рекомендуем вводить продажи в существующую точку продаж как кассовые операции." }, "bitcoin": { "question": "Что я могу делать с биткойнами?", "answer": "Потратьте их у других торговцев или оплатите ими свои расходы, попросив своих сотрудников, поставщиков и арендодателя забрать их." } }, "footer": { "company": "Компания", "followUs": "Подписывайтесь на нас", "github": "GitHub", "linktree": "Линктри", "jobs": "Вакансии", "support": "Поддерживать", "telegram": "Телеграмма", "twitter": "Твиттер", "documentation": "Документация" }, "howItWorks": { "header": "Как это работает", "step1": { "header": "1. Создайте свою учетную запись", "description": "Имя пользователя и пароль — это все, что вам нужно для начала работы." }, "step2": { "header": "2. Попросите оплату", "description": "То же самое, что и обычно, но с биткойнами." }, "step3": { "header": "3. Вот и все!", "description": "Потратьте свою монету немедленно или наблюдайте, как она растет в цене." } }, "invoice": { "addTip": "Добавить совет", "addTipq": "Добавить совет?", "done": "Сделанный", "paymentSuccessful": "Оплата получена!", "invoice": "Счет", "total": "Общий", "tip": "Кончик", "addMemo": "Добавить заметку" }, "jobs": { "header": "Вакансии", "subheader": "Текущие вакансии", "pitch1": "Coinos активно ищет разработчиков, которые присоединятся к нашей растущей команде (полный или неполный рабочий день, возможна удаленная работа).", "pitch2": "В восторге от работы над биткойнами с открытым исходным кодом? Тогда взгляните на наши репозитории на", "github": "GitHub", "pitch3": "Если вы думаете, что можете оказать влияние, мы будем рады услышать ваше мнение. Отправьте нам свое резюме на" }, "landing": { "header_with_icon": "самый простой", "header_after_icon": "способ начать работу с биткойнами.", "subheader": "Бесплатный веб-кошелек и страница оплаты для всех.", "info1": { "title": "Мгновенные платежи через Lightning", "description": "Мы упрощаем для продавцов получение розничных платежей. Не нужно ждать банковских расчетов или беспокоиться о возвратных платежах. Мгновенный расчет за считанные секунды. Попрощайтесь с комиссией по карте в размере 3%." }, "info2": { "title": "Простой в использовании", "description1": "Просто введите сумму в местной валюте, поделитесь QR-кодом, и все готово.", "description2": "Мы берем на себя всю тяжелую работу, чтобы вы могли сосредоточиться на своем бизнесе.", "description3": "У вас всегда есть возможность взять на хранение свои средства, мгновенно выведя их в любое время." }, "info3": { "title": "Брендируйте это по-своему!", "description": "Настройте внешний вид страницы своего профиля или оставьте ее по умолчанию — это полностью зависит от вас." } }, "login": { "forgot": "Забыли пароль?", "reset": "Восстановление пароля", "optional": "Необязательный", "submit": "Представлять на рассмотрение", "token": "Токен 2FA", "username": "Имя пользователя", "password": "Пароль", "newPassword": "Новый пароль", "email": "Электронная почта", "passwordRecommendation": "Мы рекомендуем использовать длинный случайный пароль, если вы собираетесь хранить средства на своем счете.", "forgotUserOrPassword": "Забыли пароль?", "haveAccount": "У вас уже есть аккаунт?", "noAccount": "У вас нет учетной записи?", "register": "регистр", "signIn": "Войти", "failed": "Ошибка входа" }, "nav": { "about": "О", "docs": "Документы", "messages": "Сообщения", "funds": "Фонды", "map": "карта", "merch": "Раскачиваться", "profile": "Профиль", "startInSeconds": "Начни за секунды", "account": "Счет", "signIn": "Войти", "signOut": "выход", "dashboard": "Панель приборов", "settings": "Настройки", "support": "Поддерживать" }, "news": { "header": "Новости", "subheader": "Последние обновления" }, "user": { "profilePage": "Страница профиля", "nostrPubkey": "Ностр публичный ключ", "requestPayment": "Запросить оплату", "welcome": "Добро пожаловать! Баланс вашего счета пуст. Вы можете начать с внесения депозита или попросить кого-нибудь заплатить вам.", "showBalance": "Показать баланс", "bitcoin": "Биткойн", "lightning": "Молния", "invoice": "Счет", "unfollow": "Отписаться", "following": "Следующий", "follow": "Следовать", "followers": "Последователи", "remove": "Удалять", "noNotes": "Пока нет заметок", "message": "Сообщение", "messages": "Сообщения", "feed": "Кормить", "notes": "Примечания", "pay": "Платить", "scanToPayBTC": "Сканируйте, чтобы оплатить биткойнами", "this": "Этот", "lightningAddress": "Молниеносный адрес", "dashboard": { "go": "Идти", "dashboard": "Панель приборов", "accountBalance": "Баланс", "conversionFeeWarning": "С вас будет взиматься комиссия за вывод средств в размере 1% от суммы.", "fetchingRate": "скорость получения...", "send": "Отправлять", "receive": "Получать", "withdraw": "Отправлять", "deposit": "Депозит", "voucher": "Ваучер", "checkout": "Проверить" }, "receive": { "lessThan1BTCWarning": "Пожалуйста, введите сумму менее 1 биткойна.", "showQR": "Показать QR-код" }, "send": { "makeQR": "QR-код", "contacts": "Контакты", "scan": "Сканировать", "paste": "Вставить", "next": "Следующий", "placeholder": "Вставьте биткойн-адрес, счет-фактуру Lightning или имя пользователя Coinos." }, "settings": { "shopify": "Интеграция Shopify", "notifications": "Уведомления о платежах", "notificationsDesc": "Получайте электронное письмо всякий раз, когда платеж поступает на ваш счет.", "seedPhrase": "Начальная фраза", "nostrKeys": "Ностр ключ", "nostrDescription": "Ваш ключ nostr используется для подписи и расшифровки сообщений, обеспечения их безопасности, иначе кто-то сможет выдать себя за вас.", "seedDescription": "Эта начальная фраза из 12 слов используется для получения вашего закрытого ключа nostr и ключей вашего самостоятельного кошелька, которые определяют ваши адреса автоматического вывода средств. Сделайте резервную копию и не позволяйте никому это увидеть.", "emailDescription": "Только для сброса пароля и уведомлений о платежах", "hideNsec": "Скрыть секретный ключ nostr", "saved": "Настройки сохранены!", "verifying": "Письмо с подтверждением отправлено", "verified": "Электронная почта подтверждена!", "revealNsec": "Раскрыть секретный ключ nostr", "hideSeed": "Скрыть начальную фразу", "revealSeed": "Раскрыть исходную фразу", "saveSettings": "Сохранить настройки", "bannerImage": "Изображение баннера", "profileImage": "Изображение профиля", "oneTimeCode": "Введите одноразовый код", "accountId": "Секрет аккаунта", "header": "Настройки", "ACCOUNT": "СЧЕТ", "POINT_OF_SALE": "ПРЕДПОЧТЕНИЯ", "SECURITY": "БЕЗОПАСНОСТЬ", "locale": "Язык", "username": "Имя пользователя", "email": "Электронная почта", "newPassword": "Изменить пароль", "about": "О", "aboutPlaceholder": "Немного о вас или вашем рабочем адресе и графике работы", "emailNotifications": "Уведомления по электронной почте", "emailNotificationsDescription": "Получайте уведомления по электронной почте, когда в вашей учетной записи происходят изменения или когда люди отправляют вам платежи.", "smsNotifications": "СМС-уведомления", "smsNotificationsDescription": "Coinos отправит вам текстовые уведомления, когда эта функция будет включена.", "theme": "Цветовая тема", "tipPrompt": "Подскажите советы", "tipPromptDescription": "Спрашивайте людей, хотят ли они добавить чаевые перед оплатой счетов.", "displayName": "Отображаемое имя", "themeDescription": "Выберите цветовую тему по умолчанию.", "localCurrency": "Валюта", "btcUnit": "Отображение биткойнов в:", "bitcoin": "биткойн", "minutes": "минуты", "hour": "час", "hours": "часы", "satoshis": "Сатоши", "securityPIN": "защитный пин-код", "securityPINDescription": "Установите PIN-код, который будет использоваться для разблокировки ваших настроек и отправки исходящих платежей.", "setPIN": "Установить PIN-код", "autoLock": "Тайм-аут", "autoLockDescription": "Время, по истечении которого ваш PIN-код необходимо будет ввести повторно:", "twofa": "Двухфакторная аутентификация", "twofaDescription": "Добавьте дополнительный уровень безопасности своему устройству, включив двухфакторную аутентификацию.", "twofaSetup": "Настройте 2FA", "twofaDisable": "Отключить 2FA", "select": "Выбирать", "verifyPIN": "Подтвердите PIN-код", "enablePIN": "Включить ПИН-код", "disablePIN": "Отключить ПИН-код", "publishNip5": "Опубликовать личность nostr", "nip5a": "Использовать", "nip5b": "в качестве вашего общедоступного адреса nostr", "destination": "Адрес автоматического вывода средств", "destinationDesc": "Адрес, на который будут отправлены выплаты", "destinationPlaceholder": "Введите адрес Bitcoin, Liquid или Lightning", "reserve": "Резервная сумма", "reserveDesc": "Сумма, которая останется на вашем счете после завершения вывода средств", "threshold": "Порог автоматического вывода средств", "thresholdDesc": "Когда ваш баланс превысит эту сумму, будет запущен вывод средств.", "autoWithdraw": "Автовывод", "autoWithdrawDescription": "Включите вывод средств на внешний адрес, когда ваш баланс достигнет заданного порога.", "memoPrompt": "Инструкции по оформлению заказа", "memoPromptPlaceholder": "например Пожалуйста, введите свое имя для заказа", "rememberFor": "Помните о", "import": "Импортировать ключ", "seconds": "секунды" }, "support": { "header": "Поддерживать", "email": "Электронная почта", "accountName": "Имя учетной записи", "message": "Сообщение", "send": "Отправлять" }, "details": "Подробности", "noMessages": "Вы еще не отправляли и не получали сообщений" }, "payments": { "networkFee": "Сетевая плата", "platformFee": "Плата за платформу", "fastest": "Самый быстрый", "fast": "Быстрый", "medium": "Середина", "slow": "Медленный", "create": "Создавать", "fund": "Фонд", "addFunds": "Добавить средства", "takeFunds": "Взять средства", "show": "Показывать", "hide": "Скрывать", "qr": "QR-код", "copyLink": "Копировать ссылку", "copyText": "Копировать текст", "openLink": "Открыть кошелек", "moreOptions": "Больше вариантов", "total": "Общий", "gainLoss": "Прибыль/убыток", "presentValue": "Приведенная стоимость", "amount": "Количество", "date": "Дата", "id": "ID транзакции", "details": "Детали транзакции", "header": "Платежи", "preimage": "Подтверждение оплаты", "maxfee": "Максимальная комиссия за маршрутизацию", "type": "Тип", "time": "Время", "sent": "Отправил", "received": "Полученный", "empty": "Транзакций пока нет.", "send": "Отправлять", "next": "Следующий", "sendTo": "Отправить", "to": "к", "pay": "Платить", "requesting": "запрашивает", "sentInvoiceTo": "Отправил счет на", "sentRequestTo": "Запрос отправлен!", "pleasePay": "Пожалуйста оплатите", "day": "День", "week": "Неделя", "month": "Месяц", "all": "Все", "export": "Экспорт", "invoice": "Счет", "readyToPay": "готов заплатить", "describeOrder": "Используйте это поле, чтобы описать ваш заказ", "requestAnInvoice": "Запросить счет", "submit": "Представлять на рассмотрение", "isRequesting": "запрашивает", "invoiceFrom": "Счет от", "awaiting": "Ожидание", "tapAnywhere": "Нажмите в любом месте, чтобы продолжить", "from": "от", "redeem": "Выкупать", "redeemed": "искуплен", "redeemedBy": "Погашен", "sendMessage": "Напиши сообщение...", "fundsClaimed": "Средства заявлены!", "print": "Распечатать", "onlyLbtc": "Мы поддерживаем только", "dontDeposit": "пока не вкладывайте другие активы", "tips": "Советы", "options": "параметры", "bump": "Дополнительный сбор", "for": "для" }, "items": { "add": "Добавить элемент", "edit": "Редактировать элемент", "name": "Имя", "price": "Цена", "submit": "Представлять на рассмотрение", "image": "Изображение" } }