diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7dfc65e5..e330c7a4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,6 +12,18 @@ + + + + + + + + + + + ) -> Unit, +) { + companion object { + private const val TAG = "BleTap" + + /** Overall budget for one transfer, either role. */ + private const val TRANSFER_TIMEOUT_MS = 20_000L + + // Fixed characteristic UUIDs living inside the per-session rendezvous service. + private val CHAR_PAYLOAD_UUID: UUID = UUID.fromString("e3c0f2a1-0b7d-4c6e-9a2f-1d5b0e7a0005") + private val CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") + + private const val HEADER_VERSION: Byte = 1 + private const val DEFAULT_MTU = 23 + private const val ATT_NOTIFY_OVERHEAD = 3 + /** + * Upper bound on one notification's value, just under the 514 bytes an + * ATT_MTU of 517 allows. + * + * This was 180 when the payload travelled as characteristic *writes*: a + * write of exactly (MTU - 3) could tip Android into a prepared "long + * write", which the GATT server did not implement. Notifications have no + * long-form equivalent - they are simply capped at MTU - 3 - so that + * constraint does not apply and the old cap only cost frames. + */ + private const val MAX_SAFE_CHUNK = 512 + + /** Sanity bound on the advertised payload length in the header. */ + private const val MAX_PAYLOAD_BYTES = 64 * 1024 + + // Every notification is framed so the receiver can join mid-stream and + // resynchronise on the next header, which is what makes retransmission work. + private const val FRAME_HEADER: Byte = 0x01 + private const val FRAME_DATA: Byte = 0x02 + + /** + * Last-resort trigger for a central that never negotiates an MTU at all. + * It must stay well clear of a real MTU exchange, which takes ~3s when an + * Android 17 phone is the central - at 2.5s this fired first and chunked + * the whole payload at the 23-byte default (211 frames instead of 24). + * The real triggers are the readiness read and [POST_MTU_PUSH_MS]. + */ + private const val PUSH_SETTLE_MS = 8_000L + + /** + * Once the MTU is known, how long to give the central to finish discovery + * and register for notifications before pushing. Measured at ~40ms on a + * Pixel 8, so this is generous. Scheduling from the MTU exchange rather + * than blindly from connect is what keeps the first attempt from chunking + * at the 23-byte default. + */ + private const val POST_MTU_PUSH_MS = 500L + + /** Release the client regardless if the disconnect callback never lands. */ + private const val CLOSE_FALLBACK_MS = 2_000L + + /** + * Keep the link up briefly after the last frame is confirmed. The ATT + * confirmation is generated by the receiver's stack before its app has + * necessarily processed the frame, so tearing down instantly could make + * the receiver see a disconnect while it still believes the transfer is + * incomplete. + */ + private const val LINGER_MS = 1_500L + + /** + * If the receiver hasn't disconnected (its only way of confirming) this + * long after a full push, send the whole payload again. An Android 17 + * central cannot signal readiness at all - every app-initiated GATT + * operation it makes is silently dropped - so the sender cannot wait to be + * told, and simply repeats until the receiver confirms or we time out. + * + * Must exceed the ~5s link supervision timeout: the receiver's disconnect + * is its only way to confirm, and the peer does not observe it for about + * four seconds, so a shorter retry retransmits a payload that already + * arrived. + */ + private const val PUSH_RETRY_MS = 6_000L + } + + private val main = Handler(Looper.getMainLooper()) + private val manager: BluetoothManager? = + context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager + private val adapter: BluetoothAdapter? get() = manager?.adapter + + private var mtu = DEFAULT_MTU + private var serviceUuid: UUID? = null + + private val transferTimeout = Runnable { onTransferTimeout() } + private val pushSettle = Runnable { beginPush() } + private val linger = Runnable { stopInternal() } + private var delivered = false + + /** + * Whether the receiver proved its app registered for notifications, by + * issuing the readiness read. Only then does an indication confirmation + * amount to app-level delivery - the ATT layer confirms and discards frames + * for handles no app has registered. Android 17 centrals can never set this. + */ + private var readyProven = false + + /** + * [readyProven] snapshotted when the current attempt started. The readiness + * read can land mid-push; sampling it live would send early frames + * unacknowledged and later ones as indications, then wrongly report the whole + * attempt as acknowledged by a registered receiver. + */ + private var attemptConfirmed = false + private val closeFallback = Runnable { stopInternal() } + private val pushRetry = Runnable { beginPush() } + private var attempt = 0 + + // --- sender (peripheral) state --- + private var gattServer: BluetoothGattServer? = null + private var advertiser: BluetoothLeAdvertiser? = null + private var advertiseCallback: AdvertiseCallback? = null + private var payloadChar: BluetoothGattCharacteristic? = null + private var connectedCentral: BluetoothDevice? = null + private var pendingBlob: ByteArray? = null + private val sendQueue = ArrayDeque() + private var pushing = false + private var allSent = false + + // --- receiver (central) state --- + private var scanner: BluetoothLeScanner? = null + private var scanCallback: ScanCallback? = null + private var gatt: BluetoothGatt? = null + private var expectingHeader = true + private var expectedLen = 0 + private val inbox = ByteArrayOutputStream() + private var assembled = false + + fun isAvailable(): Boolean { + return try { + val a = adapter ?: return false + if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) return false + a.isEnabled + } catch (e: SecurityException) { + false + } + } + + // ------------------------------------------------------------------ sender + + /** Advertise [uuidString] and push [blob] to the first central that connects. */ + fun startSending(uuidString: String, blob: ByteArray) { + stopInternal() + val a = adapter ?: return sendError("bluetooth unavailable") + val adv = a.bluetoothLeAdvertiser + ?: return sendError("BLE advertising is not supported on this device") + val uuid = parseUuid(uuidString) ?: return sendError("invalid rendezvous uuid") + logd("startSending uuid=$uuid blob=${blob.size}B") + serviceUuid = uuid + advertiser = adv + pendingBlob = blob + + val server = manager?.openGattServer(context, serverCallback) + ?: return sendError("could not open GATT server") + gattServer = server + + val service = BluetoothGattService(uuid, BluetoothGattService.SERVICE_TYPE_PRIMARY) + val payload = BluetoothGattCharacteristic( + CHAR_PAYLOAD_UUID, + // READ: the receiver reads this once it has registered, and that read + // is what tells us it is safe to start pushing - a read rather than a + // write because client writes are broken on Android 17. + // + // INDICATE: frames are sent as indications, not notifications. The + // receiver's ATT layer confirms each one automatically, with no app + // involvement, which is the only end-to-end delivery signal available + // when the peer cannot make any app-initiated GATT call at all. + BluetoothGattCharacteristic.PROPERTY_READ or + BluetoothGattCharacteristic.PROPERTY_NOTIFY or + BluetoothGattCharacteristic.PROPERTY_INDICATE, + BluetoothGattCharacteristic.PERMISSION_READ, + ) + // Declared for correctness. The receiver never writes it - Android's GATT + // server does not gate notifyCharacteristicChanged on the CCCD, and a + // client write is precisely the operation this design avoids. + payload.addDescriptor( + BluetoothGattDescriptor( + CCCD_UUID, + BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE, + ) + ) + service.addCharacteristic(payload) + payloadChar = payload + + main.postDelayed(transferTimeout, TRANSFER_TIMEOUT_MS) + // Advertising starts in onServiceAdded, so a central can never connect + // and find an empty GATT server. + server.addService(service) + } + + private fun startAdvertisingInternal() { + val adv = advertiser ?: return + val uuid = serviceUuid ?: return + val settings = AdvertiseSettings.Builder() + .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) + .setConnectable(true) + .setTimeout(0) + .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) + .build() + val data = AdvertiseData.Builder() + .setIncludeDeviceName(false) + .addServiceUuid(ParcelUuid(uuid)) + .build() + val cb = object : AdvertiseCallback() { + override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) { + logd("advertising started") + } + + override fun onStartFailure(errorCode: Int) { + sendError("advertise failed: $errorCode") + } + } + advertiseCallback = cb + adv.startAdvertising(settings, data, cb) + sendStatus("advertising") + } + + private val serverCallback = object : BluetoothGattServerCallback() { + override fun onServiceAdded(status: Int, service: BluetoothGattService) { + if (service.uuid != serviceUuid) return + if (status == BluetoothGatt.GATT_SUCCESS) { + startAdvertisingInternal() + } else { + sendError("failed to register GATT service: $status") + } + } + + override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { + logd("server conn newState=$newState status=$status allSent=$allSent") + when (newState) { + BluetoothProfile.STATE_CONNECTED -> { + connectedCentral = device + // Some stacks corrupt an active connection while still + // advertising, and we only ever serve one central. + stopAdvertising() + sendStatus("connected") + main.postDelayed(pushSettle, PUSH_SETTLE_MS) + } + BluetoothProfile.STATE_DISCONNECTED -> { + if (device != connectedCentral) return + connectedCentral = null + // The receiver disconnects once it has the whole payload - + // that is the delivery signal, since acknowledging it in-band + // would require a client write. + if (allSent) { + // Backstop: normally reportDelivered() has already fired on + // the last indication's confirmation. + reportDelivered("central disconnected after full payload") + } else if (pushing) { + sendError("receiver disconnected mid-transfer (status=$status)") + stopInternal() + } + } + } + } + + override fun onMtuChanged(device: BluetoothDevice, mtu: Int) { + logd("server mtu=$mtu") + this@BleTapController.mtu = mtu + // Do not push from here: the central negotiates the MTU *before* it + // discovers services and registers for notifications, so anything sent + // now lands on the floor. But this is the right moment to time from - + // re-arm the settle so the first attempt chunks at the real MTU rather + // than the 23-byte default. + if (!pushing && !allSent) { + main.removeCallbacks(pushSettle) + main.postDelayed(pushSettle, POST_MTU_PUSH_MS) + } + } + + override fun onCharacteristicReadRequest( + device: BluetoothDevice, + requestId: Int, + offset: Int, + characteristic: BluetoothGattCharacteristic, + ) { + gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, ByteArray(0)) + if (characteristic.uuid != CHAR_PAYLOAD_UUID) return + // Fast path: a central that can still issue reads is telling us it is + // ready, so skip the settle. Android 17 centrals never get here. + logd("receiver read the payload characteristic: ready, starting push") + readyProven = true + if (!pushing) { + main.removeCallbacks(pushSettle) + beginPush() + } + } + + override fun onNotificationSent(device: BluetoothDevice, status: Int) { + if (status != BluetoothGatt.GATT_SUCCESS) { + sendError("notification failed: $status") + stopInternal() + return + } + pushNext() + } + } + + /** + * The payload is confirmed delivered. Idempotent: the last indication's + * confirmation and the receiver's subsequent disconnect can both land here. + */ + private fun reportDelivered(why: String) { + if (delivered) return + delivered = true + logd("$why -> delivered") + main.removeCallbacks(transferTimeout) + main.removeCallbacks(pushRetry) + main.removeCallbacks(pushSettle) + sendStatus("confirmed") + main.postDelayed(linger, LINGER_MS) + } + + /** Chunk the blob for the negotiated MTU and start streaming it. */ + private fun beginPush() { + if (pushing) return + val blob = pendingBlob ?: return sendError("no payload to send") + if (connectedCentral == null) return sendError("no central connected") + pushing = true + allSent = false + attempt++ + attemptConfirmed = readyProven + sendQueue.clear() + + val header = ByteArray(6) + header[0] = FRAME_HEADER + header[1] = HEADER_VERSION + header[2] = ((blob.size ushr 24) and 0xFF).toByte() + header[3] = ((blob.size ushr 16) and 0xFF).toByte() + header[4] = ((blob.size ushr 8) and 0xFF).toByte() + header[5] = (blob.size and 0xFF).toByte() + sendQueue.addLast(header) + + // One byte of every notification is the frame tag. + val frameSize = (mtu - ATT_NOTIFY_OVERHEAD).coerceAtMost(MAX_SAFE_CHUNK).coerceAtLeast(20) + val dataSize = frameSize - 1 + var i = 0 + while (i < blob.size) { + val end = minOf(i + dataSize, blob.size) + val frame = ByteArray(end - i + 1) + frame[0] = FRAME_DATA + blob.copyInto(frame, 1, i, end) + sendQueue.addLast(frame) + i = end + } + logd( + "beginPush attempt=$attempt blob=${blob.size}B mtu=$mtu dataSize=$dataSize " + + "frames=${sendQueue.size} mode=${if (attemptConfirmed) "indicate" else "notify"}" + ) + sendStatus("writing") + pushNext() + } + + /** Send the next queued chunk; each is paced by onNotificationSent. */ + private fun pushNext() { + val server = gattServer ?: return + val device = connectedCentral ?: return + val ch = payloadChar ?: return + val chunk = sendQueue.pollFirst() + if (chunk == null) { + allSent = true + pushing = false + if (attemptConfirmed) { + // Every frame was confirmed by the receiver's ATT layer AND its app + // proved it had registered before this attempt began, so this is + // real delivery. Skip the ~4.2s supervision timeout. + reportDelivered("attempt $attempt acknowledged by a registered receiver") + } else { + // Frames were confirmed, but the receiver never proved its app was + // listening - and the ATT layer confirms then discards frames for + // unregistered handles. Not enough to claim delivery of money. Fall + // back to the disconnect, which only happens once the app has + // actually assembled the payload. + logd("attempt $attempt acknowledged, but readiness unproven - awaiting disconnect") + sendStatus("sent") + main.postDelayed(pushRetry, PUSH_RETRY_MS) + } + return + } + val ok = notifyChunk(server, device, ch, chunk) + if (!ok) { + sendError("could not queue notification") + stopInternal() + } + } + + @Suppress("DEPRECATION") + private fun notifyChunk( + server: BluetoothGattServer, + device: BluetoothDevice, + ch: BluetoothGattCharacteristic, + value: ByteArray, + ): Boolean { + // Indications only when they buy something. confirm = true makes + // onNotificationSent fire on the receiver's ATT confirmation rather than + // on local buffer availability, which lets us declare delivery on the last + // frame instead of waiting out the ~4.2s supervision timeout - but each + // one costs a round trip (~131ms measured, vs ~1.8ms for a notification). + // + // That trade only pays off when the confirmation actually means delivery, + // i.e. when the receiver proved its app had registered. Otherwise we are + // waiting for the disconnect either way, so push fast and unacknowledged. + val confirm = attemptConfirmed + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val res = server.notifyCharacteristicChanged(device, ch, confirm, value) + if (res != BluetoothStatusCodes.SUCCESS) logd("notify/indicate code=$res") + res == BluetoothStatusCodes.SUCCESS + } else { + ch.value = value + server.notifyCharacteristicChanged(device, ch, confirm) + } + } + + // ---------------------------------------------------------------- receiver + + /** Scan for [uuidString], connect, and receive the pushed payload. */ + fun startReceiving(uuidString: String) { + stopInternal() + val a = adapter ?: return sendError("bluetooth unavailable") + val s = a.bluetoothLeScanner ?: return sendError("BLE scanning not supported") + val uuid = parseUuid(uuidString) ?: return sendError("invalid rendezvous uuid") + logd("startReceiving uuid=$uuid") + serviceUuid = uuid + resetInbox() + + val filters = listOf(ScanFilter.Builder().setServiceUuid(ParcelUuid(uuid)).build()) + val settings = ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build() + val cb = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + scanner?.stopScan(this) + scanCallback = null + connectTo(result.device) + } + + override fun onScanFailed(errorCode: Int) { + sendError("scan failed: $errorCode") + } + } + scanner = s + scanCallback = cb + s.startScan(filters, settings, cb) + main.postDelayed(transferTimeout, TRANSFER_TIMEOUT_MS) + sendStatus("scanning") + } + + private fun connectTo(device: BluetoothDevice) { + sendStatus("connecting") + gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) + } else { + device.connectGatt(context, false, gattCallback) + } + } + + private val gattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) { + logd("client conn newState=$newState status=$status assembled=$assembled") + when (newState) { + BluetoothProfile.STATE_CONNECTED -> { + sendStatus("connected") + // Issue GATT operations from the main thread rather than the + // binder callback thread they arrived on. + main.post { gatt?.requestMtu(517) } + } + BluetoothProfile.STATE_DISCONNECTED -> { + if (assembled) { + // Our own terminate completing; safe to release now. + main.removeCallbacks(closeFallback) + main.post { stopInternal() } + } else { + sendError("sender disconnected before the payload arrived") + } + } + } + } + + override fun onMtuChanged(g: BluetoothGatt, mtu: Int, status: Int) { + this@BleTapController.mtu = if (status == BluetoothGatt.GATT_SUCCESS) mtu else DEFAULT_MTU + logd("client mtu=$mtu status=$status using=${this@BleTapController.mtu}") + main.post { gatt?.discoverServices() } + } + + override fun onServicesDiscovered(g: BluetoothGatt, status: Int) { + val uuid = serviceUuid ?: return sendError("no rendezvous uuid") + val service = g.getService(uuid) ?: return sendError("rendezvous service not found") + val ch = service.getCharacteristic(CHAR_PAYLOAD_UUID) + ?: return sendError("payload characteristic not found") + // Local registration only - this generates no ATT traffic. We + // deliberately do NOT write the CCCD: that is a client write, the one + // operation that is broken on Android 17, and the sender notifies + // unconditionally so no subscription is needed. + val ok = g.setCharacteristicNotification(ch, true) + logd("services discovered status=$status setNotification=$ok") + if (!ok) return sendError("could not register for notifications") + sendStatus("receiving") + // Announce readiness. Only once this read lands does the sender start + // pushing, so no chunk can arrive before we are listening. + main.post { + val gg = gatt ?: return@post + val issued = gg.readCharacteristic(ch) + logd("readiness read issued=$issued") + if (!issued) sendError("could not signal readiness to the sender") + } + } + + override fun onCharacteristicChanged( + g: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + value: ByteArray, + ) { + if (characteristic.uuid != CHAR_PAYLOAD_UUID) return + handleInbound(value) + } + + // Pre-33 notification callback; the 3-arg overload above supersedes it. + @Suppress("DEPRECATION") + override fun onCharacteristicChanged( + g: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return + if (characteristic.uuid != CHAR_PAYLOAD_UUID) return + handleInbound(characteristic.value ?: return) + } + } + + /** + * Every notification is framed, so joining part-way through a push is + * survivable: data frames received before a header are discarded, and the + * next header resets collection. The sender retransmits until we confirm by + * disconnecting, so a missed first attempt costs a retry, not the transfer. + */ + private fun handleInbound(value: ByteArray) { + if (assembled || value.isEmpty()) return + when (value[0]) { + FRAME_HEADER -> { + if (value.size < 6 || value[1] != HEADER_VERSION) { + logd("malformed header frame size=${value.size}, ignoring") + return + } + val len = ((value[2].toInt() and 0xFF) shl 24) or + ((value[3].toInt() and 0xFF) shl 16) or + ((value[4].toInt() and 0xFF) shl 8) or + (value[5].toInt() and 0xFF) + if (len <= 0 || len > MAX_PAYLOAD_BYTES) { + logd("implausible header length=$len, ignoring") + return + } + expectedLen = len + expectingHeader = false + inbox.reset() + logd("header ok expectedLen=$expectedLen") + } + FRAME_DATA -> { + // No header yet: we joined mid-push. Wait for the retransmission. + if (expectingHeader) return + inbox.write(value, 1, value.size - 1) + logd("inbound ${value.size - 1}B total=${inbox.size()}/$expectedLen") + if (inbox.size() >= expectedLen) { + val full = inbox.toByteArray() + val blob = if (full.size > expectedLen) full.copyOfRange(0, expectedLen) else full + assembled = true + main.removeCallbacks(transferTimeout) + logd("assembled ${blob.size}B, disconnecting to confirm") + sendReceived(blob) + // Disconnecting is the delivery signal the sender waits for, so + // it needs to be a clean terminate: disconnect and let the + // callback close us. Calling close() straight after disconnect() + // suppresses the terminate and the sender only notices ~4s later + // on supervision timeout, by which point it has retransmitted. + main.post { + gatt?.disconnect() + main.postDelayed(closeFallback, CLOSE_FALLBACK_MS) + } + } + } + else -> logd("unknown frame tag=${value[0]}, ignoring") + } + } + + // ------------------------------------------------------------------ common + + fun stop() { + stopInternal() + sendStatus("stopped") + } + + private fun stopAdvertising() { + try { + advertiseCallback?.let { advertiser?.stopAdvertising(it) } + } catch (e: Exception) { + Log.w(TAG, "stopAdvertising: ${e.message}") + } + advertiseCallback = null + } + + private fun stopInternal() { + main.removeCallbacks(transferTimeout) + main.removeCallbacks(pushSettle) + main.removeCallbacks(pushRetry) + main.removeCallbacks(closeFallback) + main.removeCallbacks(linger) + delivered = false + readyProven = false + attemptConfirmed = false + attempt = 0 + + try { + scanCallback?.let { scanner?.stopScan(it) } + } catch (e: Exception) { + Log.w(TAG, "stopScan: ${e.message}") + } + scanCallback = null + scanner = null + + try { + gatt?.disconnect() + gatt?.close() + } catch (e: Exception) { + Log.w(TAG, "gatt close: ${e.message}") + } + gatt = null + + stopAdvertising() + advertiser = null + + try { + gattServer?.close() + } catch (e: Exception) { + Log.w(TAG, "gattServer close: ${e.message}") + } + gattServer = null + payloadChar = null + connectedCentral = null + + sendQueue.clear() + pendingBlob = null + pushing = false + allSent = false + serviceUuid = null + assembled = false + resetInbox() + mtu = DEFAULT_MTU + } + + private fun onTransferTimeout() { + logd("transfer timed out pushing=$pushing allSent=$allSent queued=${sendQueue.size} assembled=$assembled") + sendError("tap transfer timed out") + stopInternal() + } + + private fun resetInbox() { + expectingHeader = true + expectedLen = 0 + inbox.reset() + } + + private fun parseUuid(value: String): UUID? = try { + UUID.fromString(value) + } catch (e: IllegalArgumentException) { + null + } + + private fun send(map: Map) = main.post { emit(map) } + private fun sendStatus(state: String) = send(mapOf("event" to "status", "state" to state)) + + /** Log to logcat only (adb logcat -s BleTap). The native BLE trace stays out + * of the in-app log so it doesn't flood it during a multi-chunk transfer. */ + private fun logd(message: String) { + Log.i(TAG, message) + } + private fun sendReceived(blob: ByteArray) = send(mapOf("event" to "received", "data" to blob)) + private fun sendError(message: String) { + Log.w(TAG, message) + main.removeCallbacks(transferTimeout) + main.removeCallbacks(pushSettle) + main.removeCallbacks(pushRetry) + send(mapOf("event" to "error", "message" to message)) + } +} diff --git a/android/app/src/main/kotlin/app/ecash/EcashHceService.kt b/android/app/src/main/kotlin/app/ecash/EcashHceService.kt index f8917d13..9667d4d5 100644 --- a/android/app/src/main/kotlin/app/ecash/EcashHceService.kt +++ b/android/app/src/main/kotlin/app/ecash/EcashHceService.kt @@ -27,6 +27,15 @@ class EcashHceService : HostApduService() { @Volatile var ndefMessage: ByteArray? = null + /** + * Invoked when a reader has actually read the NDEF payload, i.e. a tap + * just happened. MainActivity forwards it to Dart so the receiver can + * start scanning for the sender's advertisement. Fires once per read, so + * the Dart side must be idempotent. + */ + @Volatile + var onTagRead: (() -> Unit)? = null + private val SW_OK = byteArrayOf(0x90.toByte(), 0x00.toByte()) private val SW_NOT_FOUND = byteArrayOf(0x6A.toByte(), 0x82.toByte()) @@ -124,6 +133,8 @@ class EcashHceService : HostApduService() { Selected.CC -> CC_CONTENT Selected.NDEF -> { val msg = ndefMessage ?: return reply("READ no NDEF message", SW_NOT_FOUND) + // A reader is pulling the rendezvous: the tap is happening now. + if (offset >= 2) onTagRead?.invoke() // The NDEF file is NLEN (2 bytes, big-endian) followed by the message. val nlen = msg.size byteArrayOf(((nlen ushr 8) and 0xFF).toByte(), (nlen and 0xFF).toByte()) + msg diff --git a/android/app/src/main/kotlin/app/ecash/MainActivity.kt b/android/app/src/main/kotlin/app/ecash/MainActivity.kt index ade2333c..846a0506 100644 --- a/android/app/src/main/kotlin/app/ecash/MainActivity.kt +++ b/android/app/src/main/kotlin/app/ecash/MainActivity.kt @@ -6,11 +6,14 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.nfc.NfcAdapter +import android.nfc.Tag import android.nfc.cardemulation.CardEmulation +import android.nfc.tech.Ndef import android.util.Log import android.view.WindowManager import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodChannel class MainActivity : FlutterActivity() { @@ -36,6 +39,13 @@ class MainActivity : FlutterActivity() { private var nfcPendingIntent: PendingIntent? = null private var nfcIntentFilters: Array? = null + private var bleController: BleTapController? = null + private var bleEventSink: EventChannel.EventSink? = null + + private var nfcTapEventSink: EventChannel.EventSink? = null + private var hceEventSink: EventChannel.EventSink? = null + private var readerModeActive = false + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) @@ -65,6 +75,23 @@ class MainActivity : FlutterActivity() { }, ) + // The receiver has to know the moment its rendezvous was read over NFC: + // under the sender-is-peripheral design it must start scanning then, and + // scanning continuously would be a battery problem. + EventChannel(flutterEngine.dartExecutor.binaryMessenger, "ecashapp/nfc_hce/events") + .setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + hceEventSink = events + } + + override fun onCancel(arguments: Any?) { + hceEventSink = null + } + }) + EcashHceService.onTagRead = { + runOnUiThread { hceEventSink?.success(mapOf("event" to "tagRead")) } + } + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "ecashapp/nfc_hce") .setMethodCallHandler { call, result -> when (call.method) { @@ -101,6 +128,98 @@ class MainActivity : FlutterActivity() { else -> result.notImplemented() } } + + // BLE "tap to send" transport (Phase 2). Events stream to Dart over an + // EventChannel; the controller posts them on the main thread already. + EventChannel(flutterEngine.dartExecutor.binaryMessenger, "ecashapp/ble_tap/events") + .setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + bleEventSink = events + } + + override fun onCancel(arguments: Any?) { + bleEventSink = null + } + }) + + val ble = BleTapController(applicationContext) { event -> bleEventSink?.success(event) } + bleController = ble + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "ecashapp/ble_tap") + .setMethodCallHandler { call, result -> + when (call.method) { + "isAvailable" -> result.success(ble.isAvailable()) + "startReceiving" -> { + val uuid = call.argument("uuid") + if (uuid == null) { + result.error("missing_uuid", "uuid required", null) + } else { + ble.startReceiving(uuid) + result.success(null) + } + } + "startSending" -> { + val uuid = call.argument("uuid") + val blob = call.argument("blob") + if (uuid == null || blob == null) { + result.error("missing_args", "uuid and blob required", null) + } else { + ble.startSending(uuid, blob) + result.success(null) + } + } + "stop" -> { + ble.stop() + result.success(null) + } + else -> result.notImplemented() + } + } + + // NFC reader mode for the "tap to send" handshake (Phase 3). The sender + // reads the receiver's rendezvous (ephemeral pubkey + BLE service UUID) + // off an emulated NDEF tag. Reader mode and foreground dispatch are + // mutually exclusive, so `readerModeActive` gates onResume/onPause. + EventChannel(flutterEngine.dartExecutor.binaryMessenger, "ecashapp/nfc_tap/events") + .setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + nfcTapEventSink = events + } + + override fun onCancel(arguments: Any?) { + nfcTapEventSink = null + } + }) + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "ecashapp/nfc_tap") + .setMethodCallHandler { call, result -> + when (call.method) { + "startReader" -> { + val adapter = nfcAdapter + if (adapter == null) { + result.error("no_nfc", "NFC unavailable", null) + } else { + readerModeActive = true + adapter.disableForegroundDispatch(this) + enableReaderModeInternal(adapter) + result.success(null) + } + } + "stopReader" -> { + readerModeActive = false + nfcAdapter?.disableReaderMode(this) + nfcAdapter?.enableForegroundDispatch( + this, nfcPendingIntent, nfcIntentFilters, null, + ) + result.success(null) + } + else -> result.notImplemented() + } + } + } + + override fun onDestroy() { + bleController?.stop() + super.onDestroy() } /** @@ -137,17 +256,63 @@ class MainActivity : FlutterActivity() { override fun onResume() { super.onResume() - nfcAdapter?.enableForegroundDispatch( - this, - nfcPendingIntent, - nfcIntentFilters, - null, - ) + val adapter = nfcAdapter ?: return + if (readerModeActive) { + enableReaderModeInternal(adapter) + } else { + adapter.enableForegroundDispatch(this, nfcPendingIntent, nfcIntentFilters, null) + } } override fun onPause() { super.onPause() - nfcAdapter?.disableForegroundDispatch(this) + val adapter = nfcAdapter ?: return + if (readerModeActive) { + adapter.disableReaderMode(this) + } else { + adapter.disableForegroundDispatch(this) + } + } + + private fun enableReaderModeInternal(adapter: NfcAdapter) { + val flags = NfcAdapter.FLAG_READER_NFC_A or + NfcAdapter.FLAG_READER_NFC_B or + NfcAdapter.FLAG_READER_NFC_F or + NfcAdapter.FLAG_READER_NFC_V + adapter.enableReaderMode(this, { tag -> if (tag != null) onTagRead(tag) }, flags, null) + } + + /** + * Read the receiver's rendezvous URI (`ecashtap:`) off an emulated + * NDEF tag and forward it to Dart, which decodes the pubkey + BLE UUID. + */ + private fun onTagRead(tag: Tag) { + val ndef = Ndef.get(tag) + if (ndef == null) { + emitNfcTap(mapOf("event" to "error", "message" to "tag is not NDEF")) + return + } + try { + ndef.connect() + val uri = ndef.ndefMessage?.records?.firstOrNull()?.toUri()?.toString() + if (uri != null) { + emitNfcTap(mapOf("event" to "read", "uri" to uri)) + } else { + emitNfcTap(mapOf("event" to "error", "message" to "no URI record on tag")) + } + } catch (e: Exception) { + emitNfcTap(mapOf("event" to "error", "message" to (e.message ?: "NFC read failed"))) + } finally { + try { + ndef.close() + } catch (e: Exception) { + Log.w("NfcTap", "ndef close: ${e.message}") + } + } + } + + private fun emitNfcTap(event: Map) { + runOnUiThread { nfcTapEventSink?.success(event) } } /** diff --git a/lib/app.dart b/lib/app.dart index c4ff5309..6a7a5919 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -19,6 +19,7 @@ import 'package:ecashapp/providers/preferences_provider.dart'; import 'package:ecashapp/scan.dart'; import 'package:ecashapp/setttings.dart'; import 'package:ecashapp/sidebar.dart'; +import 'package:ecashapp/tap_transfer/tap_receive.dart'; import 'package:ecashapp/theme.dart'; import 'package:ecashapp/toast.dart'; import 'package:ecashapp/utils.dart'; @@ -43,7 +44,7 @@ class MyApp extends StatefulWidget { State createState() => _MyAppState(); } -class _MyAppState extends State { +class _MyAppState extends State with WidgetsBindingObserver { late List<(FederationSelector, bool)> _feds; int _refreshTrigger = 0; FederationSelector? _selectedFederation; @@ -74,6 +75,12 @@ class _MyAppState extends State { super.initState(); _feds = widget.initialFederations; + // Passive "tap to receive" — armed while the app is foregrounded and BLE + // permissions are already granted (never prompts). See tap_receive.dart. + WidgetsBinding.instance.addObserver(this); + TapReceive.instance.onEcash = _handleReceivedTapEcash; + TapReceive.instance.arm(); + if (_feds.isNotEmpty) { _selectedFederation = _feds.first.$1; _isRecovering = _feds.first.$2; @@ -341,6 +348,9 @@ class _MyAppState extends State { @override void dispose() { + WidgetsBinding.instance.removeObserver(this); + TapReceive.instance.onEcash = null; + TapReceive.instance.disarm(); _subscription.cancel(); _deepLinkSubscription?.cancel(); _peerStatusSubscription?.cancel(); @@ -349,6 +359,26 @@ class _MyAppState extends State { super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + TapReceive.instance.arm(); + } else { + TapReceive.instance.disarm(); + } + } + + /// Passive receiver got a tapped ecash token: present the same redeem UI the + /// QR scanner uses (no silent auto-reissue). Suppress the funds-received toast + /// while the redeem sheet is up, matching the scan flow. + Future _handleReceivedTapEcash(String ecash) async { + final ctx = _navigatorKey.currentContext; + if (ctx == null) return; + invoicePaidToastVisible.value = false; + await presentReceivedEcash(ctx, ecash); + invoicePaidToastVisible.value = true; + } + void _checkPendingDeepLink() { final pendingDeepLink = DeepLinkHandler().pendingDeepLink; if (pendingDeepLink != null) { diff --git a/lib/db.dart b/lib/db.dart index 0dade80a..8c22f1ae 100644 --- a/lib/db.dart +++ b/lib/db.dart @@ -7,8 +7,8 @@ import 'frb_generated.dart'; import 'lib.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `BitcoinDisplayKey`, `BtcPriceKey`, `BtcPrice`, `BtcPricesKey`, `BtcPrices`, `ContactKeyPrefix`, `ContactKey`, `ContactSyncConfigKey`, `DbKeyPrefix`, `FederationBackupKey`, `FederationConfigKeyPrefix`, `FederationConfigKey`, `FederationMetaKeyPrefix`, `FederationMetaKey`, `FederationOrderKey`, `FederationOrder`, `FiatCurrencyKey`, `LightningAddressKeyPrefix`, `NostrRelaysKeyPrefix`, `NostrRelaysKey`, `NostrWalletConnectConfig`, `NostrWalletConnectKeyPrefix`, `NostrWalletConnectKey`, `NostrWalletConnectV2Config`, `NostrWalletConnectV2KeyPrefix`, `NostrWalletConnectV2Key`, `NwcLimitsKey`, `NwcLimits`, `NwcSpendWindowKey`, `NwcSpendWindow`, `PinAttemptsKey`, `PinAttempts`, `PinCodeHashKey`, `PinCredentialKey`, `RequirePinForSpendingKey`, `SchemaVersionKey`, `SeedPhraseAckKey`, `ShowMsatsKey`, `Timestamp`, `WalletV2PendingDepositFederationPrefix`, `WalletV2PendingDepositKey` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `cmp`, `cmp`, `cmp`, `cmp`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `hash`, `hash`, `hash`, `partial_cmp`, `partial_cmp`, `partial_cmp`, `partial_cmp` +// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `BitcoinDisplayKey`, `BtcPriceKey`, `BtcPrice`, `BtcPricesKey`, `BtcPrices`, `ContactKeyPrefix`, `ContactKey`, `ContactSyncConfigKey`, `DbKeyPrefix`, `FederationBackupKey`, `FederationConfigKeyPrefix`, `FederationConfigKey`, `FederationMetaKeyPrefix`, `FederationMetaKey`, `FederationOrderKey`, `FederationOrder`, `FiatCurrencyKey`, `LightningAddressKeyPrefix`, `NostrRelaysKeyPrefix`, `NostrRelaysKey`, `NostrWalletConnectConfig`, `NostrWalletConnectKeyPrefix`, `NostrWalletConnectKey`, `NostrWalletConnectV2Config`, `NostrWalletConnectV2KeyPrefix`, `NostrWalletConnectV2Key`, `NwcLimitsKey`, `NwcLimits`, `NwcSpendWindowKey`, `NwcSpendWindow`, `PinAttemptsKey`, `PinAttempts`, `PinCodeHashKey`, `PinCredentialKey`, `RequirePinForSpendingKey`, `SchemaVersionKey`, `SeedPhraseAckKey`, `ShowMsatsKey`, `TapReceiveEnabledKey`, `Timestamp`, `WalletV2PendingDepositFederationPrefix`, `WalletV2PendingDepositKey` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `cmp`, `cmp`, `cmp`, `cmp`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_decode_partial_from_finite_reader`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `consensus_encode`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `hash`, `hash`, `hash`, `partial_cmp`, `partial_cmp`, `partial_cmp`, `partial_cmp` // Rust type: RustOpaqueMoi> abstract class FederationConfig implements RustOpaqueInterface { diff --git a/lib/ecash_send.dart b/lib/ecash_send.dart index df0ca37e..d5ce0112 100644 --- a/lib/ecash_send.dart +++ b/lib/ecash_send.dart @@ -9,6 +9,9 @@ import 'package:ecashapp/fountain.dart'; import 'package:ecashapp/qr_export.dart'; import 'package:ecashapp/lib.dart'; import 'package:ecashapp/multimint.dart'; +import 'package:ecashapp/tap_transfer/ble_tap.dart'; +import 'package:ecashapp/tap_transfer/tap_nfc.dart'; +import 'package:ecashapp/tap_transfer/tap_receive.dart'; import 'package:ecashapp/toast.dart'; import 'package:ecashapp/utils.dart'; import 'package:ecashapp/utils/pin_guard.dart'; @@ -16,6 +19,7 @@ import 'package:ecashapp/extensions/build_context_l10n.dart'; import 'package:ecashapp/widgets/secure_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; @@ -51,9 +55,20 @@ class _EcashSendState extends State { bool _copied = false; _QrMode _mode = _QrMode.legacy; + // Tap-to-send (NFC handshake + BLE). Additive: the QR is untouched and stays + // the fallback. Armed once the notes are generated, if BLE is available. + StreamSubscription? _tapBleSub; + StreamSubscription? _tapNfcSub; + bool _tapAvailable = false; + bool _tapSending = false; + String? _tapStatus; + @override void initState() { super.initState(); + // The send screen needs exclusive NFC/BLE (reader mode + outgoing GATT), so + // pause the passive receiver while it's open. + TapReceive.instance.pause(); _loadQuote(); } @@ -118,6 +133,7 @@ class _EcashSendState extends State { _fragmentStream = _createFrameStream(encoder, legacyFrames); _generating = false; }); + unawaited(_armTap()); } catch (e) { AppLogger.instance.error("Could not send Ecash: $e"); if (mounted) showErrorToast(context, e); @@ -158,6 +174,158 @@ class _EcashSendState extends State { }); } + @override + void dispose() { + _tapBleSub?.cancel(); + _tapNfcSub?.cancel(); + TapNfc.stopReader(); + BleTap.stop(); + TapReceive.instance.resume(); + super.dispose(); + } + + /// Arm NFC reader mode on the QR screen so a receiver can tap to pull the + /// ecash over BLE. Additive: the QR remains the primary/fallback path. Only + /// enabled when BLE is present and enabled. + Future _armTap() async { + if (!await BleTap.isAvailable()) return; + if (!mounted) return; + _tapBleSub = BleTap.events().listen(_onTapBleEvent); + _tapNfcSub = TapNfc.reads().listen(_onTapRendezvous); + setState(() => _tapAvailable = true); + try { + await TapNfc.startReader(); + } catch (e) { + AppLogger.instance.warn("Tap send: could not start NFC reader: $e"); + } + } + + /// A receiver tapped: encrypt the already-generated notes for its NFC-delivered + /// pubkey and stream them over BLE. BLE permission is requested here — only + /// once a real tap happens — rather than up front. + Future _onTapRendezvous(TapRendezvous r) async { + if (_tapSending || _notes == null) return; + _tapSending = true; + AppLogger.instance.info( + "tap: read rendezvous uuid=${r.uuid} pubkey=${r.pubkey.length}B", + ); + await TapNfc.stopReader(); + if (!await _ensureBlePermissions()) { + _tapSending = false; + if (mounted) await TapNfc.startReader(); + return; + } + try { + final blob = encryptEcashForTap( + ecash: _notes!.toString(), + recipientPubkey: r.pubkey, + ); + AppLogger.instance.info("tap: encrypted ${blob.length}B, advertising"); + if (mounted) setState(() => _tapStatus = context.l10n.tapConnecting); + await BleTap.startSending(r.uuid, blob); + } catch (e) { + AppLogger.instance.error("Tap send failed: $e"); + _tapSending = false; + if (mounted) { + setState(() => _tapStatus = null); + showErrorToast(context, e); + await TapNfc.startReader(); + } + } + } + + void _onTapBleEvent(BleTapEvent e) { + if (!mounted) return; + switch (e.event) { + case 'status': + switch (e.state) { + case 'confirmed': + _onTapSuccess(); + break; + case 'writing': + case 'sent': + setState(() => _tapStatus = context.l10n.tapSending); + break; + case 'advertising': + case 'scanning': + case 'connecting': + case 'connected': + setState(() => _tapStatus = context.l10n.tapConnecting); + break; + } + break; + case 'error': + _tapSending = false; + AppLogger.instance.warn("tap send failed: ${e.message}"); + setState(() => _tapStatus = null); + ToastService().show( + message: context.l10n.tapSendFailed, + duration: const Duration(seconds: 3), + onTap: () {}, + icon: Icon(Icons.error), + ); + TapNfc.startReader(); + break; + } + } + + void _onTapSuccess() { + if (!mounted) return; + final prefs = context.read(); + final message = context.l10n.amountSpent( + formatBalance( + _notes!.amountMsats(), + prefs.showMsats, + prefs.bitcoinDisplay, + ), + ); + Navigator.of(context).popUntil((route) => route.isFirst); + ToastService().show( + message: message, + duration: const Duration(seconds: 5), + onTap: () {}, + icon: Icon(Icons.currency_bitcoin), + ); + } + + Future _ensureBlePermissions() async { + final statuses = + await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + // The sender advertises the rendezvous now: it is the GATT peripheral. + Permission.bluetoothAdvertise, + ].request(); + return statuses.values.every((s) => s.isGranted); + } + + Widget _buildTapStatus(ThemeData theme) { + final sending = _tapStatus != null; + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (sending) + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + Icon(Icons.contactless, size: 20, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Flexible( + child: Text( + sending ? _tapStatus! : context.l10n.tapToSendHint, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ), + ], + ); + } + Widget _buildLoading(String message) { return Center( child: Column( @@ -343,6 +511,10 @@ class _EcashSendState extends State { setState(() => _mode = selection.first); }, ), + if (_tapAvailable) ...[ + const SizedBox(height: 16), + _buildTapStatus(theme), + ], const SizedBox(height: 24), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), diff --git a/lib/frb_generated.dart b/lib/frb_generated.dart index ef36ae65..3c0a5324 100644 --- a/lib/frb_generated.dart +++ b/lib/frb_generated.dart @@ -17,6 +17,7 @@ import 'lnurl_client.dart'; import 'multimint.dart'; import 'nostr.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +import 'tap_transfer.dart'; /// Main entrypoint of the Rust API class RustLib extends BaseEntrypoint { @@ -69,7 +70,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.9.0'; @override - int get rustContentHash => 1098215331; + int get rustContentHash => 586808380; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -446,6 +447,10 @@ abstract class RustLibApi extends BaseApi { Future crateMultimintMultimintGetShowMsats({required Multimint that}); + Future crateMultimintMultimintGetTapReceiveEnabled({ + required Multimint that, + }); + Future crateMultimintMultimintGuardianAddGateway({ required Multimint that, required FederationId federationId, @@ -663,6 +668,11 @@ abstract class RustLibApi extends BaseApi { required bool showMsats, }); + Future crateMultimintMultimintSetTapReceiveEnabled({ + required Multimint that, + required bool enabled, + }); + Future> crateMultimintMultimintTransactions({ required Multimint that, required FederationId federationId, @@ -876,6 +886,15 @@ abstract class RustLibApi extends BaseApi { String? picture, }); + String crateTapTransferTapRecipientDecrypt({ + required TapRecipient that, + required List blob, + }); + + TapRecipient crateTapTransferTapRecipientNew(); + + Uint8List crateTapTransferTapRecipientPublicKey({required TapRecipient that}); + BigInt crateMultimintWithdrawFeesResponseAutoAccessorGetFederationFeeMsats({ required WithdrawFeesResponse that, }); @@ -1018,6 +1037,11 @@ abstract class RustLibApi extends BaseApi { required bool isDesktop, }); + Uint8List crateEncryptEcashForTap({ + required String ecash, + required List recipientPubkey, + }); + Future crateExecuteLnurlWithdraw({ required FederationId federationId, required String callback, @@ -1109,6 +1133,8 @@ abstract class RustLibApi extends BaseApi { Future crateGetShowMsats(); + Future crateGetTapReceiveEnabled(); + Future crateGuardianAddGateway({ required FederationId federationId, required int peer, @@ -1341,6 +1367,8 @@ abstract class RustLibApi extends BaseApi { Future crateSetShowMsats({required bool showMsats}); + Future crateSetTapReceiveEnabled({required bool enabled}); + Stream crateSubscribeDeposits({ required FederationId federationId, }); @@ -1633,6 +1661,14 @@ abstract class RustLibApi extends BaseApi { CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_SafeUrlPtr; + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_TapRecipient; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_TapRecipient; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_TapRecipientPtr; + RustArcIncrementStrongCountFnType get rust_arc_increment_strong_count_WithdrawFees; @@ -4518,6 +4554,42 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["that"], ); + @override + Future crateMultimintMultimintGetTapReceiveEnabled({ + required Multimint that, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMultimint( + that, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 76, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bool, + decodeErrorData: null, + ), + constMeta: kCrateMultimintMultimintGetTapReceiveEnabledConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateMultimintMultimintGetTapReceiveEnabledConstMeta => + const TaskConstMeta( + debugName: "Multimint_get_tap_receive_enabled", + argNames: ["that"], + ); + @override Future crateMultimintMultimintGuardianAddGateway({ required Multimint that, @@ -4544,7 +4616,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 76, + funcId: 77, port: port_, ); }, @@ -4589,7 +4661,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 77, + funcId: 78, port: port_, ); }, @@ -4635,7 +4707,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 78, + funcId: 79, port: port_, ); }, @@ -4678,7 +4750,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 79, + funcId: 80, port: port_, ); }, @@ -4723,7 +4795,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 80, + funcId: 81, port: port_, ); }, @@ -4770,7 +4842,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 81, + funcId: 82, port: port_, ); }, @@ -4819,7 +4891,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 82, + funcId: 83, port: port_, ); }, @@ -4871,7 +4943,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 83, + funcId: 84, port: port_, ); }, @@ -4916,7 +4988,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 84, + funcId: 85, port: port_, ); }, @@ -4963,7 +5035,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 85, + funcId: 86, port: port_, ); }, @@ -5008,7 +5080,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 86, + funcId: 87, port: port_, ); }, @@ -5044,7 +5116,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 87, + funcId: 88, port: port_, ); }, @@ -5084,7 +5156,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 88, + funcId: 89, port: port_, ); }, @@ -5126,7 +5198,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 89, + funcId: 90, port: port_, ); }, @@ -5176,7 +5248,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 90, + funcId: 91, port: port_, ); }, @@ -5214,7 +5286,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 91, + funcId: 92, port: port_, ); }, @@ -5260,7 +5332,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 92, + funcId: 93, port: port_, ); }, @@ -5313,7 +5385,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 93, + funcId: 94, port: port_, ); }, @@ -5369,7 +5441,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 94, + funcId: 95, port: port_, ); }, @@ -5431,7 +5503,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 95, + funcId: 96, port: port_, ); }, @@ -5476,7 +5548,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 96, + funcId: 97, port: port_, ); }, @@ -5512,7 +5584,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 97, + funcId: 98, port: port_, ); }, @@ -5553,7 +5625,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 98, + funcId: 99, port: port_, ); }, @@ -5603,7 +5675,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 99, + funcId: 100, port: port_, ); }, @@ -5662,7 +5734,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 100, + funcId: 101, port: port_, ); }, @@ -5701,7 +5773,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 101, + funcId: 102, port: port_, ); }, @@ -5759,7 +5831,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 102, + funcId: 103, port: port_, ); }, @@ -5825,7 +5897,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 103, + funcId: 104, port: port_, ); }, @@ -5864,7 +5936,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 104, + funcId: 105, port: port_, ); }, @@ -5905,7 +5977,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 105, + funcId: 106, port: port_, ); }, @@ -5943,7 +6015,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 106, + funcId: 107, port: port_, ); }, @@ -5981,7 +6053,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 107, + funcId: 108, port: port_, ); }, @@ -6002,6 +6074,44 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["that", "showMsats"], ); + @override + Future crateMultimintMultimintSetTapReceiveEnabled({ + required Multimint that, + required bool enabled, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMultimint( + that, + serializer, + ); + sse_encode_bool(enabled, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 109, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateMultimintMultimintSetTapReceiveEnabledConstMeta, + argValues: [that, enabled], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateMultimintMultimintSetTapReceiveEnabledConstMeta => + const TaskConstMeta( + debugName: "Multimint_set_tap_receive_enabled", + argNames: ["that", "enabled"], + ); + @override Future> crateMultimintMultimintTransactions({ required Multimint that, @@ -6028,7 +6138,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 108, + funcId: 110, port: port_, ); }, @@ -6077,7 +6187,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 109, + funcId: 111, port: port_, ); }, @@ -6129,7 +6239,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 110, + funcId: 112, port: port_, ); }, @@ -6182,7 +6292,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 111, + funcId: 113, port: port_, ); }, @@ -6218,7 +6328,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 112, + funcId: 114, port: port_, ); }, @@ -6256,7 +6366,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 113, + funcId: 115, port: port_, ); }, @@ -6292,7 +6402,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 114, + funcId: 116, port: port_, ); }, @@ -6328,7 +6438,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 115, + funcId: 117, port: port_, ); }, @@ -6366,7 +6476,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 116, + funcId: 118, port: port_, ); }, @@ -6402,7 +6512,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 117, + funcId: 119, port: port_, ); }, @@ -6440,7 +6550,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 118, + funcId: 120, port: port_, ); }, @@ -6475,7 +6585,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 119, + funcId: 121, port: port_, ); }, @@ -6514,7 +6624,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 120, + funcId: 122, port: port_, ); }, @@ -6551,7 +6661,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 121, + funcId: 123, port: port_, ); }, @@ -6587,7 +6697,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 122, + funcId: 124, port: port_, ); }, @@ -6625,7 +6735,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 123, + funcId: 125, port: port_, ); }, @@ -6665,7 +6775,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 124, + funcId: 126, port: port_, ); }, @@ -6705,7 +6815,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 125, + funcId: 127, port: port_, ); }, @@ -6747,7 +6857,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 126, + funcId: 128, port: port_, ); }, @@ -6788,7 +6898,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 127, + funcId: 129, port: port_, ); }, @@ -6826,7 +6936,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 128, + funcId: 130, port: port_, ); }, @@ -6866,7 +6976,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 129, + funcId: 131, port: port_, ); }, @@ -6911,7 +7021,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 130, + funcId: 132, port: port_, ); }, @@ -6945,7 +7055,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 131, + funcId: 133, port: port_, ); }, @@ -6983,7 +7093,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 132, + funcId: 134, port: port_, ); }, @@ -7021,7 +7131,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 133, + funcId: 135, )!; }, codec: SseCodec( @@ -7051,7 +7161,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 134, + funcId: 136, )!; }, codec: SseCodec( @@ -7084,7 +7194,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 135, + funcId: 137, )!; }, codec: SseCodec( @@ -7120,7 +7230,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 136, + funcId: 138, port: port_, ); }, @@ -7156,7 +7266,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 137, + funcId: 139, )!; }, codec: SseCodec( @@ -7191,7 +7301,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 138, + funcId: 140, )!; }, codec: SseCodec( @@ -7226,7 +7336,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 139, + funcId: 141, )!; }, codec: SseCodec( @@ -7261,7 +7371,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 140, + funcId: 142, )!; }, codec: SseCodec( @@ -7299,7 +7409,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 141, + funcId: 143, )!; }, codec: SseCodec( @@ -7336,7 +7446,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 142, + funcId: 144, )!; }, codec: SseCodec( @@ -7373,7 +7483,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 143, + funcId: 145, )!; }, codec: SseCodec( @@ -7409,7 +7519,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 144, + funcId: 146, )!; }, codec: SseCodec( @@ -7445,7 +7555,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 145, + funcId: 147, )!; }, codec: SseCodec( @@ -7483,7 +7593,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 146, + funcId: 148, )!; }, codec: SseCodec( @@ -7523,7 +7633,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 147, + funcId: 149, )!; }, codec: SseCodec( @@ -7562,7 +7672,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 148, + funcId: 150, )!; }, codec: SseCodec( @@ -7601,7 +7711,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 149, + funcId: 151, )!; }, codec: SseCodec( @@ -7640,7 +7750,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 150, + funcId: 152, )!; }, codec: SseCodec( @@ -7678,7 +7788,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 151, + funcId: 153, )!; }, codec: SseCodec( @@ -7716,7 +7826,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 152, + funcId: 154, )!; }, codec: SseCodec( @@ -7737,6 +7847,105 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["that", "picture"], ); + @override + String crateTapTransferTapRecipientDecrypt({ + required TapRecipient that, + required List blob, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + that, + serializer, + ); + sse_encode_list_prim_u_8_loose(blob, serializer); + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 155, + )!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_ecash_app_error, + ), + constMeta: kCrateTapTransferTapRecipientDecryptConstMeta, + argValues: [that, blob], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateTapTransferTapRecipientDecryptConstMeta => + const TaskConstMeta( + debugName: "TapRecipient_decrypt", + argNames: ["that", "blob"], + ); + + @override + TapRecipient crateTapTransferTapRecipientNew() { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 156, + )!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient, + decodeErrorData: null, + ), + constMeta: kCrateTapTransferTapRecipientNewConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateTapTransferTapRecipientNewConstMeta => + const TaskConstMeta(debugName: "TapRecipient_new", argNames: []); + + @override + Uint8List crateTapTransferTapRecipientPublicKey({ + required TapRecipient that, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + that, + serializer, + ); + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 157, + )!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeErrorData: null, + ), + constMeta: kCrateTapTransferTapRecipientPublicKeyConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateTapTransferTapRecipientPublicKeyConstMeta => + const TaskConstMeta( + debugName: "TapRecipient_public_key", + argNames: ["that"], + ); + @override BigInt crateMultimintWithdrawFeesResponseAutoAccessorGetFederationFeeMsats({ required WithdrawFeesResponse that, @@ -7752,7 +7961,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 153, + funcId: 158, )!; }, codec: SseCodec( @@ -7790,7 +7999,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 154, + funcId: 159, )!; }, codec: SseCodec( @@ -7827,7 +8036,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 155, + funcId: 160, )!; }, codec: SseCodec( @@ -7865,7 +8074,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 156, + funcId: 161, )!; }, codec: SseCodec( @@ -7903,7 +8112,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 157, + funcId: 162, )!; }, codec: SseCodec( @@ -7942,7 +8151,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 158, + funcId: 163, )!; }, codec: SseCodec( @@ -7982,7 +8191,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 159, + funcId: 164, )!; }, codec: SseCodec( @@ -8021,7 +8230,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 160, + funcId: 165, )!; }, codec: SseCodec( @@ -8064,7 +8273,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 161, + funcId: 166, )!; }, codec: SseCodec( @@ -8103,7 +8312,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 162, + funcId: 167, )!; }, codec: SseCodec( @@ -8134,7 +8343,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 163, + funcId: 168, port: port_, ); }, @@ -8162,7 +8371,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 164, + funcId: 169, port: port_, ); }, @@ -8195,7 +8404,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 165, + funcId: 170, port: port_, ); }, @@ -8236,7 +8445,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 166, + funcId: 171, port: port_, ); }, @@ -8276,7 +8485,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 167, + funcId: 172, port: port_, ); }, @@ -8317,7 +8526,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 168, + funcId: 173, port: port_, ); }, @@ -8357,7 +8566,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 169, + funcId: 174, port: port_, ); }, @@ -8386,7 +8595,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 170, + funcId: 175, port: port_, ); }, @@ -8417,7 +8626,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 171, + funcId: 176, port: port_, ); }, @@ -8452,7 +8661,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 172, + funcId: 177, port: port_, ); }, @@ -8490,7 +8699,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 173, + funcId: 178, port: port_, ); }, @@ -8530,7 +8739,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 174, + funcId: 179, port: port_, ); }, @@ -8565,7 +8774,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 175, + funcId: 180, port: port_, ); }, @@ -8602,7 +8811,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 176, + funcId: 181, port: port_, ); }, @@ -8645,7 +8854,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 177, + funcId: 182, port: port_, ); }, @@ -8697,7 +8906,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 178, + funcId: 183, port: port_, ); }, @@ -8726,7 +8935,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 179, + funcId: 184, port: port_, ); }, @@ -8757,7 +8966,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 180, + funcId: 185, port: port_, ); }, @@ -8798,7 +9007,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 181, + funcId: 186, port: port_, ); }, @@ -8834,7 +9043,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 182, + funcId: 187, port: port_, ); }, @@ -8868,7 +9077,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 183, + funcId: 188, port: port_, ); }, @@ -8903,7 +9112,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 184, + funcId: 189, port: port_, ); }, @@ -8923,6 +9132,39 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["path", "isDesktop"], ); + @override + Uint8List crateEncryptEcashForTap({ + required String ecash, + required List recipientPubkey, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(ecash, serializer); + sse_encode_list_prim_u_8_loose(recipientPubkey, serializer); + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 190, + )!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeErrorData: sse_decode_ecash_app_error, + ), + constMeta: kCrateEncryptEcashForTapConstMeta, + argValues: [ecash, recipientPubkey], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateEncryptEcashForTapConstMeta => const TaskConstMeta( + debugName: "encrypt_ecash_for_tap", + argNames: ["ecash", "recipientPubkey"], + ); + @override Future crateExecuteLnurlWithdraw({ required FederationId federationId, @@ -8954,7 +9196,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 185, + funcId: 191, port: port_, ); }, @@ -9010,7 +9252,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 186, + funcId: 192, port: port_, ); }, @@ -9039,7 +9281,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 187, + funcId: 193, port: port_, ); }, @@ -9068,7 +9310,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 188, + funcId: 194, port: port_, ); }, @@ -9101,7 +9343,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 189, + funcId: 195, port: port_, ); }, @@ -9131,7 +9373,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 190, + funcId: 196, port: port_, ); }, @@ -9158,7 +9400,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 191, + funcId: 197, port: port_, ); }, @@ -9185,7 +9427,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 192, + funcId: 198, port: port_, ); }, @@ -9212,7 +9454,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 193, + funcId: 199, port: port_, ); }, @@ -9239,7 +9481,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 194, + funcId: 200, port: port_, ); }, @@ -9275,7 +9517,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 195, + funcId: 201, port: port_, ); }, @@ -9305,7 +9547,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 196, + funcId: 202, port: port_, ); }, @@ -9333,7 +9575,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 197, + funcId: 203, port: port_, ); }, @@ -9368,7 +9610,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 198, + funcId: 204, port: port_, ); }, @@ -9407,7 +9649,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 199, + funcId: 205, port: port_, ); }, @@ -9443,7 +9685,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 200, + funcId: 206, port: port_, ); }, @@ -9481,7 +9723,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 201, + funcId: 207, port: port_, ); }, @@ -9511,7 +9753,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 202, + funcId: 208, port: port_, ); }, @@ -9546,7 +9788,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 203, + funcId: 209, port: port_, ); }, @@ -9582,7 +9824,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 204, + funcId: 210, port: port_, ); }, @@ -9612,7 +9854,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 205, + funcId: 211, port: port_, ); }, @@ -9640,7 +9882,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 206, + funcId: 212, port: port_, ); }, @@ -9667,7 +9909,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 207, + funcId: 213, port: port_, ); }, @@ -9700,7 +9942,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 208, + funcId: 214, port: port_, ); }, @@ -9729,7 +9971,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 209, + funcId: 215, port: port_, ); }, @@ -9756,7 +9998,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 210, + funcId: 216, port: port_, ); }, @@ -9771,14 +10013,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } - TaskConstMeta get kCrateGetRequirePinForSpendingConstMeta => - const TaskConstMeta( - debugName: "get_require_pin_for_spending", - argNames: [], - ); + TaskConstMeta get kCrateGetRequirePinForSpendingConstMeta => + const TaskConstMeta( + debugName: "get_require_pin_for_spending", + argNames: [], + ); + + @override + Future crateGetShowMsats() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 217, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bool, + decodeErrorData: null, + ), + constMeta: kCrateGetShowMsatsConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateGetShowMsatsConstMeta => + const TaskConstMeta(debugName: "get_show_msats", argNames: []); @override - Future crateGetShowMsats() { + Future crateGetTapReceiveEnabled() { return handler.executeNormal( NormalTask( callFfi: (port_) { @@ -9786,7 +10055,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 211, + funcId: 218, port: port_, ); }, @@ -9794,15 +10063,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeSuccessData: sse_decode_bool, decodeErrorData: null, ), - constMeta: kCrateGetShowMsatsConstMeta, + constMeta: kCrateGetTapReceiveEnabledConstMeta, argValues: [], apiImpl: this, ), ); } - TaskConstMeta get kCrateGetShowMsatsConstMeta => - const TaskConstMeta(debugName: "get_show_msats", argNames: []); + TaskConstMeta get kCrateGetTapReceiveEnabledConstMeta => + const TaskConstMeta(debugName: "get_tap_receive_enabled", argNames: []); @override Future crateGuardianAddGateway({ @@ -9825,7 +10094,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 212, + funcId: 219, port: port_, ); }, @@ -9864,7 +10133,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 213, + funcId: 220, port: port_, ); }, @@ -9903,7 +10172,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 214, + funcId: 221, port: port_, ); }, @@ -9941,7 +10210,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 215, + funcId: 222, port: port_, ); }, @@ -9980,7 +10249,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 216, + funcId: 223, port: port_, ); }, @@ -10021,7 +10290,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 217, + funcId: 224, port: port_, ); }, @@ -10064,7 +10333,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 218, + funcId: 225, port: port_, ); }, @@ -10104,7 +10373,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 219, + funcId: 226, port: port_, ); }, @@ -10143,7 +10412,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 220, + funcId: 227, port: port_, ); }, @@ -10184,7 +10453,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 221, + funcId: 228, port: port_, ); }, @@ -10223,7 +10492,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 222, + funcId: 229, port: port_, ); }, @@ -10252,7 +10521,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 223, + funcId: 230, port: port_, ); }, @@ -10279,7 +10548,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 224, + funcId: 231, port: port_, ); }, @@ -10306,7 +10575,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 225, + funcId: 232, port: port_, ); }, @@ -10334,7 +10603,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 226, + funcId: 233, port: port_, ); }, @@ -10362,7 +10631,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 227, + funcId: 234, )!; }, codec: SseCodec( @@ -10393,7 +10662,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 228, + funcId: 235, port: port_, ); }, @@ -10427,7 +10696,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 229, + funcId: 236, port: port_, ); }, @@ -10459,7 +10728,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 230, + funcId: 237, port: port_, ); }, @@ -10498,7 +10767,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 231, + funcId: 238, port: port_, ); }, @@ -10530,7 +10799,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 232, + funcId: 239, port: port_, ); }, @@ -10560,7 +10829,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 233, + funcId: 240, port: port_, ); }, @@ -10594,7 +10863,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 234, + funcId: 241, port: port_, ); }, @@ -10635,7 +10904,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 235, + funcId: 242, port: port_, ); }, @@ -10673,7 +10942,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 236, + funcId: 243, port: port_, ); }, @@ -10713,7 +10982,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 237, + funcId: 244, port: port_, ); }, @@ -10756,7 +11025,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 238, + funcId: 245, )!; }, codec: SseCodec( @@ -10791,7 +11060,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 239, + funcId: 246, port: port_, ); }, @@ -10825,7 +11094,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 240, + funcId: 247, port: port_, ); }, @@ -10861,7 +11130,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 241, + funcId: 248, port: port_, ); }, @@ -10891,7 +11160,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 242, + funcId: 249, port: port_, ); }, @@ -10936,7 +11205,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 243, + funcId: 250, port: port_, ); }, @@ -10990,7 +11259,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 244, + funcId: 251, port: port_, ); }, @@ -11019,7 +11288,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 245, + funcId: 252, port: port_, ); }, @@ -11052,7 +11321,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 246, + funcId: 253, port: port_, ); }, @@ -11096,7 +11365,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 247, + funcId: 254, port: port_, ); }, @@ -11147,7 +11416,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 248, + funcId: 255, port: port_, ); }, @@ -11177,7 +11446,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 249, + funcId: 256, port: port_, ); }, @@ -11213,7 +11482,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 250, + funcId: 257, port: port_, ); }, @@ -11244,7 +11513,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 251, + funcId: 258, port: port_, ); }, @@ -11291,7 +11560,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 252, + funcId: 259, port: port_, ); }, @@ -11349,7 +11618,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 253, + funcId: 260, port: port_, ); }, @@ -11382,7 +11651,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 254, + funcId: 261, port: port_, ); }, @@ -11415,7 +11684,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 255, + funcId: 262, port: port_, ); }, @@ -11445,7 +11714,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 256, + funcId: 263, port: port_, ); }, @@ -11484,7 +11753,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 257, + funcId: 264, port: port_, ); }, @@ -11514,7 +11783,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 258, + funcId: 265, port: port_, ); }, @@ -11544,7 +11813,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 259, + funcId: 266, port: port_, ); }, @@ -11574,7 +11843,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 260, + funcId: 267, port: port_, ); }, @@ -11606,7 +11875,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 261, + funcId: 268, port: port_, ); }, @@ -11637,7 +11906,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 262, + funcId: 269, port: port_, ); }, @@ -11655,6 +11924,36 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateSetShowMsatsConstMeta => const TaskConstMeta(debugName: "set_show_msats", argNames: ["showMsats"]); + @override + Future crateSetTapReceiveEnabled({required bool enabled}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_bool(enabled, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 270, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateSetTapReceiveEnabledConstMeta, + argValues: [enabled], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateSetTapReceiveEnabledConstMeta => const TaskConstMeta( + debugName: "set_tap_receive_enabled", + argNames: ["enabled"], + ); + @override Stream crateSubscribeDeposits({ required FederationId federationId, @@ -11673,7 +11972,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 263, + funcId: 271, port: port_, ); }, @@ -11707,7 +12006,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 264, + funcId: 272, port: port_, ); }, @@ -11750,7 +12049,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 265, + funcId: 273, port: port_, ); }, @@ -11792,7 +12091,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 266, + funcId: 274, port: port_, ); }, @@ -11825,7 +12124,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 267, + funcId: 275, port: port_, ); }, @@ -11864,7 +12163,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 268, + funcId: 276, port: port_, ); }, @@ -11894,7 +12193,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 269, + funcId: 277, port: port_, ); }, @@ -11922,7 +12221,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 270, + funcId: 278, port: port_, ); }, @@ -11957,7 +12256,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 271, + funcId: 279, port: port_, ); }, @@ -12003,7 +12302,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 272, + funcId: 280, port: port_, ); }, @@ -12045,7 +12344,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 273, + funcId: 281, port: port_, ); }, @@ -12295,6 +12594,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { get rust_arc_decrement_strong_count_SafeUrl => wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSafeUrl; + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_TapRecipient => + wire.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_TapRecipient => + wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient; + RustArcIncrementStrongCountFnType get rust_arc_increment_strong_count_WithdrawFees => wire.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees; @@ -12585,6 +12892,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return SafeUrlImpl.frbInternalDcoDecode(raw as List); } + @protected + TapRecipient + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return TapRecipientImpl.frbInternalDcoDecode(raw as List); + } + @protected WithdrawFees dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -12823,6 +13139,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return SafeUrlImpl.frbInternalDcoDecode(raw as List); } + @protected + TapRecipient + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return TapRecipientImpl.frbInternalDcoDecode(raw as List); + } + @protected WithdrawFeesResponse dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFeesResponse( @@ -13109,6 +13434,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return SafeUrlImpl.frbInternalDcoDecode(raw as List); } + @protected + TapRecipient + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return TapRecipientImpl.frbInternalDcoDecode(raw as List); + } + @protected WithdrawFees dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -13885,6 +14219,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as Uint16List; } + @protected + List dco_decode_list_prim_u_8_loose(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as List; + } + @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -15186,6 +15526,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + TapRecipient + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return TapRecipientImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + @protected WithdrawFees sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -15498,6 +15850,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + TapRecipient + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return TapRecipientImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + @protected WithdrawFeesResponse sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFeesResponse( @@ -15858,6 +16222,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + TapRecipient + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return TapRecipientImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + @protected WithdrawFees sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -16820,6 +17196,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getUint16List(len_); } + @protected + List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getUint8List(len_); + } + @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -18304,6 +18687,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + TapRecipient self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as TapRecipientImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + @protected void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -18642,6 +19038,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + TapRecipient self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as TapRecipientImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + @protected void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFeesResponse( @@ -19038,6 +19447,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + TapRecipient self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as TapRecipientImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + @protected void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -19936,6 +20358,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putUint16List(self); } + @protected + void sse_encode_list_prim_u_8_loose( + List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putUint8List( + self is Uint8List ? self : Uint8List.fromList(self), + ); + } + @protected void sse_encode_list_prim_u_8_strict( Uint8List self, @@ -21930,6 +22364,9 @@ class MultimintImpl extends RustOpaque implements Multimint { Future getShowMsats() => RustLib.instance.api.crateMultimintMultimintGetShowMsats(that: this); + Future getTapReceiveEnabled() => RustLib.instance.api + .crateMultimintMultimintGetTapReceiveEnabled(that: this); + /// Returns false when the gateway was already whitelisted. Future guardianAddGateway({ required FederationId federationId, @@ -22305,6 +22742,12 @@ class MultimintImpl extends RustOpaque implements Multimint { Future setShowMsats({required bool showMsats}) => RustLib.instance.api .crateMultimintMultimintSetShowMsats(that: this, showMsats: showMsats); + Future setTapReceiveEnabled({required bool enabled}) => + RustLib.instance.api.crateMultimintMultimintSetTapReceiveEnabled( + that: this, + enabled: enabled, + ); + Future> transactions({ required FederationId federationId, BigInt? timestamp, @@ -22761,6 +23204,35 @@ class SafeUrlImpl extends RustOpaque implements SafeUrl { ); } +@sealed +class TapRecipientImpl extends RustOpaque implements TapRecipient { + // Not to be used by end users + TapRecipientImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + TapRecipientImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_TapRecipient, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_TapRecipient, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_TapRecipientPtr, + ); + + /// Decrypt a blob produced by [`encrypt_ecash`], returning the original + /// ecash string ready to pass to `reissue_ecash`. + String decrypt({required List blob}) => RustLib.instance.api + .crateTapTransferTapRecipientDecrypt(that: this, blob: blob); + + /// The 33-byte compressed public key to hand to the sender over NFC. + Uint8List publicKey() => + RustLib.instance.api.crateTapTransferTapRecipientPublicKey(that: this); +} + @sealed class WithdrawFeesImpl extends RustOpaque implements WithdrawFees { // Not to be used by end users diff --git a/lib/frb_generated.io.dart b/lib/frb_generated.io.dart index 63658f9d..c2d69dc8 100644 --- a/lib/frb_generated.io.dart +++ b/lib/frb_generated.io.dart @@ -16,6 +16,7 @@ import 'lnurl_client.dart'; import 'multimint.dart'; import 'nostr.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; +import 'tap_transfer.dart'; abstract class RustLibApiImplPlatform extends BaseApiImpl { RustLibApiImplPlatform({ @@ -136,6 +137,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_SafeUrlPtr => wire._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSafeUrlPtr; + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_TapRecipientPtr => + wire._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipientPtr; + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_WithdrawFeesPtr => wire._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFeesPtr; @@ -315,6 +320,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + TapRecipient + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + dynamic raw, + ); + @protected WithdrawFees dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -471,6 +482,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + TapRecipient + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + dynamic raw, + ); + @protected WithdrawFeesResponse dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFeesResponse( @@ -651,6 +668,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + TapRecipient + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + dynamic raw, + ); + @protected WithdrawFees dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -930,6 +953,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint16List dco_decode_list_prim_u_16_strict(dynamic raw); + @protected + List dco_decode_list_prim_u_8_loose(dynamic raw); + @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); @@ -1387,6 +1413,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + TapRecipient + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + SseDeserializer deserializer, + ); + @protected WithdrawFees sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -1543,6 +1575,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + TapRecipient + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + SseDeserializer deserializer, + ); + @protected WithdrawFeesResponse sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFeesResponse( @@ -1723,6 +1761,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + TapRecipient + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + SseDeserializer deserializer, + ); + @protected WithdrawFees sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -2052,6 +2096,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint16List sse_decode_list_prim_u_16_strict(SseDeserializer deserializer); + @protected + List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer); + @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); @@ -2560,6 +2607,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + TapRecipient self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -2742,6 +2796,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + TapRecipient self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFeesResponse( @@ -2952,6 +3013,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + TapRecipient self, + SseSerializer serializer, + ); + @protected void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -3354,6 +3422,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_list_prim_u_8_loose(List self, SseSerializer serializer); + @protected void sse_encode_list_prim_u_8_strict( Uint8List self, @@ -4734,6 +4805,40 @@ class RustLibWire implements BaseWire { _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSafeUrlPtr .asFunction)>(); + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipientPtr = + _lookup)>>( + 'frbgen_ecashapp_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipientPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipientPtr = + _lookup)>>( + 'frbgen_ecashapp_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipientPtr + .asFunction)>(); + void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( ffi.Pointer ptr, diff --git a/lib/frb_generated.web.dart b/lib/frb_generated.web.dart index 11ab9d09..eff142b0 100644 --- a/lib/frb_generated.web.dart +++ b/lib/frb_generated.web.dart @@ -18,6 +18,7 @@ import 'lnurl_client.dart'; import 'multimint.dart'; import 'nostr.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; +import 'tap_transfer.dart'; abstract class RustLibApiImplPlatform extends BaseApiImpl { RustLibApiImplPlatform({ @@ -138,6 +139,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_SafeUrlPtr => wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSafeUrl; + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_TapRecipientPtr => + wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient; + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_WithdrawFeesPtr => wire.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees; @@ -317,6 +322,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + TapRecipient + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + dynamic raw, + ); + @protected WithdrawFees dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -473,6 +484,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + TapRecipient + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + dynamic raw, + ); + @protected WithdrawFeesResponse dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFeesResponse( @@ -653,6 +670,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + TapRecipient + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + dynamic raw, + ); + @protected WithdrawFees dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -932,6 +955,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint16List dco_decode_list_prim_u_16_strict(dynamic raw); + @protected + List dco_decode_list_prim_u_8_loose(dynamic raw); + @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); @@ -1389,6 +1415,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + TapRecipient + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + SseDeserializer deserializer, + ); + @protected WithdrawFees sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -1545,6 +1577,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + TapRecipient + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + SseDeserializer deserializer, + ); + @protected WithdrawFeesResponse sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFeesResponse( @@ -1725,6 +1763,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + TapRecipient + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + SseDeserializer deserializer, + ); + @protected WithdrawFees sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -2054,6 +2098,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint16List sse_decode_list_prim_u_16_strict(SseDeserializer deserializer); + @protected + List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer); + @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); @@ -2562,6 +2609,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + TapRecipient self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -2744,6 +2798,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + TapRecipient self, + SseSerializer serializer, + ); + @protected void sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFeesResponse( @@ -2954,6 +3015,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + TapRecipient self, + SseSerializer serializer, + ); + @protected void sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( @@ -3356,6 +3424,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_list_prim_u_8_loose(List self, SseSerializer serializer); + @protected void sse_encode_list_prim_u_8_strict( Uint8List self, @@ -4205,6 +4276,22 @@ class RustLibWire implements BaseWire { ptr, ); + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + ptr, + ); + void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( int ptr, @@ -4534,6 +4621,16 @@ extension type RustLibWasmModule._(JSObject _) implements JSObject { int ptr, ); + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + int ptr, + ); + external void rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( int ptr, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a7883d30..3a37bb13 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1321,6 +1321,27 @@ "ecashCopiedToClipboard": "Ecash copied to clipboard", "@ecashCopiedToClipboard": {}, + "tapToSendHint": "Or tap the recipient's phone", + "@tapToSendHint": {}, + + "tapConnecting": "Connecting…", + "@tapConnecting": {}, + + "tapSending": "Sending…", + "@tapSending": {}, + + "tapSendFailed": "Tap-to-send failed — use the QR code", + "@tapSendFailed": {}, + + "tapToReceiveTitle": "Tap to receive", + "@tapToReceiveTitle": {}, + + "tapToReceiveSubtitle": "Let others tap your phone to send you ecash", + "@tapToReceiveSubtitle": {}, + + "tapToReceivePermissionDenied": "Bluetooth permission is required for tap to receive", + "@tapToReceivePermissionDenied": {}, + "federationLabel": "Federation", "@federationLabel": {}, diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 136bb1ad..2d81d82c 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -373,6 +373,13 @@ "failedToLoadEcash": "Error al cargar Ecash", "ecashWithdrawn": "Ecash retirado", "ecashCopiedToClipboard": "Ecash copiado al portapapeles", + "tapToSendHint": "O toca el teléfono del destinatario", + "tapConnecting": "Conectando…", + "tapSending": "Enviando…", + "tapSendFailed": "Falló el envío por toque — usa el código QR", + "tapToReceiveTitle": "Tocar para recibir", + "tapToReceiveSubtitle": "Permite que otros toquen tu teléfono para enviarte ecash", + "tapToReceivePermissionDenied": "Se requiere permiso de Bluetooth para tocar y recibir", "federationLabel": "Federación", "confirmPayment": "Confirmar pago", "amountSpent": "{amount} gastado", diff --git a/lib/lib.dart b/lib/lib.dart index 83e539be..b6711492 100644 --- a/lib/lib.dart +++ b/lib/lib.dart @@ -262,6 +262,18 @@ Future sendEcash({ feeMsats: feeMsats, ); +/// Encrypt an ecash string for a tap-transfer recipient (Phase 1 of the NFC + +/// BLE "tap to send" feature). `recipient_pubkey` is the 33-byte compressed key +/// received over NFC; the returned blob is delivered to the receiver over BLE +/// and decrypted with `TapRecipient::decrypt`. See `tap_transfer.rs`. +Uint8List encryptEcashForTap({ + required String ecash, + required List recipientPubkey, +}) => RustLib.instance.api.crateEncryptEcashForTap( + ecash: ecash, + recipientPubkey: recipientPubkey, +); + Future calculateEcashReissueFees({ required FederationId federationId, required String ecash, @@ -654,6 +666,12 @@ Future getShowMsats() => RustLib.instance.api.crateGetShowMsats(); Future setShowMsats({required bool showMsats}) => RustLib.instance.api.crateSetShowMsats(showMsats: showMsats); +Future getTapReceiveEnabled() => + RustLib.instance.api.crateGetTapReceiveEnabled(); + +Future setTapReceiveEnabled({required bool enabled}) => + RustLib.instance.api.crateSetTapReceiveEnabled(enabled: enabled); + Future hasPinCode() => RustLib.instance.api.crateHasPinCode(); /// Enroll a PIN for the first time. diff --git a/lib/multimint.dart b/lib/multimint.dart index 824f1a20..09aa50b4 100644 --- a/lib/multimint.dart +++ b/lib/multimint.dart @@ -264,6 +264,8 @@ abstract class Multimint implements RustOpaqueInterface { Future getShowMsats(); + Future getTapReceiveEnabled(); + /// Returns false when the gateway was already whitelisted. Future guardianAddGateway({ required FederationId federationId, @@ -499,6 +501,8 @@ abstract class Multimint implements RustOpaqueInterface { Future setShowMsats({required bool showMsats}); + Future setTapReceiveEnabled({required bool enabled}); + Future> transactions({ required FederationId federationId, BigInt? timestamp, diff --git a/lib/providers/preferences_provider.dart b/lib/providers/preferences_provider.dart index abdcadc5..29a98a1a 100644 --- a/lib/providers/preferences_provider.dart +++ b/lib/providers/preferences_provider.dart @@ -7,11 +7,13 @@ class PreferencesProvider extends ChangeNotifier { BitcoinDisplay _bitcoinDisplay = BitcoinDisplay.bip177; FiatCurrency _fiatCurrency = FiatCurrency.usd; bool _showMsats = false; + bool _tapReceiveEnabled = false; bool _isLoading = true; BitcoinDisplay get bitcoinDisplay => _bitcoinDisplay; FiatCurrency get fiatCurrency => _fiatCurrency; bool get showMsats => _showMsats; + bool get tapReceiveEnabled => _tapReceiveEnabled; bool get isLoading => _isLoading; PreferencesProvider() { @@ -23,6 +25,7 @@ class PreferencesProvider extends ChangeNotifier { _bitcoinDisplay = await rust_lib.getBitcoinDisplay(); _fiatCurrency = await rust_lib.getFiatCurrency(); _showMsats = await rust_lib.getShowMsats(); + _tapReceiveEnabled = await rust_lib.getTapReceiveEnabled(); _isLoading = false; notifyListeners(); } catch (e) { @@ -61,4 +64,14 @@ class PreferencesProvider extends ChangeNotifier { AppLogger.instance.error('Failed to save show msats preference: $e'); } } + + Future setTapReceiveEnabled(bool value) async { + _tapReceiveEnabled = value; + notifyListeners(); + try { + await rust_lib.setTapReceiveEnabled(enabled: value); + } catch (e) { + AppLogger.instance.error('Failed to save tap receive preference: $e'); + } + } } diff --git a/lib/request.dart b/lib/request.dart index 58cb67c2..3c636f56 100644 --- a/lib/request.dart +++ b/lib/request.dart @@ -7,6 +7,7 @@ import 'package:ecashapp/extensions/build_context_l10n.dart'; import 'package:ecashapp/lib.dart'; import 'package:ecashapp/multimint.dart'; import 'package:ecashapp/nfc_hce.dart'; +import 'package:ecashapp/tap_transfer/tap_receive.dart'; import 'package:ecashapp/providers/preferences_provider.dart'; import 'package:ecashapp/success.dart'; import 'package:ecashapp/toast.dart'; @@ -63,6 +64,9 @@ class _RequestState extends State _startCountdown(); _waitForPayment(); WidgetsBinding.instance.addObserver(this); + // The Lightning-invoice HCE and the passive receiver both drive the single + // HCE service, so pause the receiver while this screen owns the tag. + TapReceive.instance.pause(); _startNfcBroadcast(); } @@ -71,6 +75,7 @@ class _RequestState extends State _timer?.cancel(); WidgetsBinding.instance.removeObserver(this); InvoiceNfcBroadcaster.stop(); + TapReceive.instance.resume(); super.dispose(); } diff --git a/lib/screens/display_settings.dart b/lib/screens/display_settings.dart index 69847fe4..6924622c 100644 --- a/lib/screens/display_settings.dart +++ b/lib/screens/display_settings.dart @@ -1,7 +1,9 @@ import 'package:ecashapp/db.dart'; import 'package:ecashapp/extensions/build_context_l10n.dart'; import 'package:ecashapp/providers/preferences_provider.dart'; +import 'package:ecashapp/tap_transfer/tap_transfer_dev_screen.dart'; import 'package:ecashapp/toast.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -501,6 +503,32 @@ class _DisplaySettingsScreenState extends State { ], ), ), + // Debug-only harness for the NFC + BLE tap-to-send feature (Phase 2). + if (kDebugMode) ...[ + const SizedBox(height: 24), + Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: ListTile( + leading: Icon( + Icons.wifi_tethering, + color: theme.colorScheme.primary, + ), + title: const Text('Tap transfer (dev)'), // i18n-ignore + subtitle: const Text('BLE ecash transfer test'), // i18n-ignore + trailing: const Icon(Icons.chevron_right), + onTap: + () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const TapTransferDevScreen(), + ), + ), + ), + ), + ], ], ), ); diff --git a/lib/setttings.dart b/lib/setttings.dart index 54a158db..06c7b8e3 100644 --- a/lib/setttings.dart +++ b/lib/setttings.dart @@ -5,14 +5,18 @@ import 'package:ecashapp/ln_address.dart'; import 'package:ecashapp/mnemonic.dart'; import 'package:ecashapp/multimint.dart'; import 'package:ecashapp/nwc.dart'; +import 'package:ecashapp/providers/preferences_provider.dart'; import 'package:ecashapp/relays.dart'; import 'package:ecashapp/screens/access_control.dart'; import 'package:ecashapp/screens/btcmap_screen.dart'; import 'package:ecashapp/screens/display_settings.dart'; +import 'package:ecashapp/tap_transfer/tap_receive.dart'; import 'package:ecashapp/theme.dart'; +import 'package:ecashapp/toast.dart'; import 'package:ecashapp/utils/pin_guard.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:provider/provider.dart'; class SettingsScreen extends StatefulWidget { final void Function(FederationSelector fed, bool recovering) onJoin; @@ -52,6 +56,31 @@ class _SettingsScreenState extends State { }); } + /// Toggle passive "tap to receive". Enabling requests the BLE permissions the + /// receiver needs (the one place we prompt) before persisting and arming. + Future _toggleTapReceive(bool value) async { + final prefs = context.read(); + if (value) { + final granted = await TapReceive.instance.requestPermissions(); + if (!granted) { + if (mounted) { + ToastService().show( + message: context.l10n.tapToReceivePermissionDenied, + duration: const Duration(seconds: 4), + onTap: () {}, + icon: const Icon(Icons.error), + ); + } + return; + } + await prefs.setTapReceiveEnabled(true); + await TapReceive.instance.arm(); + } else { + await prefs.setTapReceiveEnabled(false); + await TapReceive.instance.disarm(); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -156,6 +185,22 @@ class _SettingsScreenState extends State { ); }, ), + Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: SwitchListTile( + secondary: Icon( + Icons.contactless, + color: Theme.of(context).colorScheme.primary, + ), + title: Text(context.l10n.tapToReceiveTitle), + subtitle: Text(context.l10n.tapToReceiveSubtitle), + value: context.watch().tapReceiveEnabled, + onChanged: _toggleTapReceive, + ), + ), _SettingsOption( icon: Icon( Icons.lock, diff --git a/lib/tap_transfer.dart b/lib/tap_transfer.dart new file mode 100644 index 00000000..26f66d48 --- /dev/null +++ b/lib/tap_transfer.dart @@ -0,0 +1,23 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.9.0. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import 'app_error.dart'; +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// These functions are ignored because they are not marked as `pub`: `decrypt`, `derive_key`, `encrypt_ecash`, `encrypt` + +// Rust type: RustOpaqueMoi> +abstract class TapRecipient implements RustOpaqueInterface { + /// Decrypt a blob produced by [`encrypt_ecash`], returning the original + /// ecash string ready to pass to `reissue_ecash`. + String decrypt({required List blob}); + + factory TapRecipient() => + RustLib.instance.api.crateTapTransferTapRecipientNew(); + + /// The 33-byte compressed public key to hand to the sender over NFC. + Uint8List publicKey(); +} diff --git a/lib/tap_transfer/ble_tap.dart b/lib/tap_transfer/ble_tap.dart new file mode 100644 index 00000000..c09532e8 --- /dev/null +++ b/lib/tap_transfer/ble_tap.dart @@ -0,0 +1,72 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; + +/// An event streamed from the native BLE tap-transfer controller. +/// +/// `event` is one of: `status`, `received`, `error`. +/// - `status` → [state] set (advertising/scanning/connecting/connected/writing/sent/confirmed/stopped) +/// - `received`→ [data] is the fully reassembled encrypted blob (receiver side) +/// - `error` → [message] set +class BleTapEvent { + final String event; + final String? state; + final String? message; + final Uint8List? data; + + const BleTapEvent({required this.event, this.state, this.message, this.data}); + + factory BleTapEvent.fromMap(Map map) => BleTapEvent( + event: map['event'] as String, + state: map['state'] as String?, + message: map['message'] as String?, + data: map['data'] as Uint8List?, + ); + + @override + String toString() => + 'BleTapEvent($event${state != null ? ' $state' : ''}' + '${message != null ? ' "$message"' : ''}' + '${data != null ? ' ${data!.length}B' : ''})'; +} + +/// Thin Dart wrapper over the native `ecashapp/ble_tap` channels (Android only). +/// +/// See android/app/src/main/kotlin/app/ecash/BleTapController.kt. This is the +/// Phase 2 transport: it moves an already-encrypted blob between two phones over +/// a no-bond GATT connection. Encryption/decryption itself lives in Rust +/// (`TapRecipient` / `encryptEcashForTap`). +class BleTap { + static const MethodChannel _method = MethodChannel('ecashapp/ble_tap'); + static const EventChannel _events = EventChannel('ecashapp/ble_tap/events'); + + /// Broadcast stream of controller events. Safe to listen to before starting. + static Stream events() => _events.receiveBroadcastStream().map( + (e) => BleTapEvent.fromMap(e as Map), + ); + + /// Whether BLE is present and enabled on this device. + static Future isAvailable() async => + (await _method.invokeMethod('isAvailable')) ?? false; + + /// Sender: advertise the per-session rendezvous [serviceUuid] (learned over + /// NFC) and push [blob] — already encrypted for the pubkey that came with it — + /// to the first central that connects. + /// + /// The sender is the peripheral because Android 17's GATT *client* accepts + /// attribute writes and never delivers their callback, so the payload travels + /// as server-to-client notifications instead. See BleTapController.kt. + static Future startSending(String serviceUuid, Uint8List blob) => + _method.invokeMethod('startSending', { + 'uuid': serviceUuid, + 'blob': blob, + }); + + /// Receiver: scan for [serviceUuid], connect (no bond), and collect the pushed + /// blob, which arrives as a `received` event. + static Future startReceiving(String serviceUuid) => + _method.invokeMethod('startReceiving', {'uuid': serviceUuid}); + + /// Tear down whichever role is active and release BLE resources. + static Future stop() => _method.invokeMethod('stop'); +} diff --git a/lib/tap_transfer/tap_nfc.dart b/lib/tap_transfer/tap_nfc.dart new file mode 100644 index 00000000..758187e9 --- /dev/null +++ b/lib/tap_transfer/tap_nfc.dart @@ -0,0 +1,128 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; + +/// The handshake payload exchanged over NFC: the receiver's ephemeral public key +/// plus the per-session BLE rendezvous service UUID. +class TapRendezvous { + final Uint8List pubkey; // 33-byte compressed secp256k1 key + final String uuid; // BLE rendezvous service UUID + + const TapRendezvous({required this.pubkey, required this.uuid}); +} + +/// NFC side of "tap to send" (Phase 3), Android only. +/// +/// Receiver publishes the rendezvous over HCE (reusing the existing +/// `ecashapp/nfc_hce` channel, encoded as an `ecashtap:` URI record). +/// Sender enters reader mode (`ecashapp/nfc_tap`) and, on tap, reads that URI and +/// decodes it back into a [TapRendezvous]. The pubkey only ever crosses this +/// proximity-authenticated channel — never BLE — which is what makes the +/// transfer MITM-safe. +class TapNfc { + static const MethodChannel _hce = MethodChannel('ecashapp/nfc_hce'); + static const MethodChannel _reader = MethodChannel('ecashapp/nfc_tap'); + static const EventChannel _readerEvents = EventChannel( + 'ecashapp/nfc_tap/events', + ); + static const EventChannel _hceEvents = EventChannel( + 'ecashapp/nfc_hce/events', + ); + + static const int _version = 1; + static const String _scheme = 'ecashtap:'; + static const int _rendezvousLen = 1 + 33 + 16; // version + pubkey + uuid + + /// Whether NFC + HCE is available (receiver publish path). + static Future hceAvailable() async { + try { + return (await _hce.invokeMethod('isAvailable')) ?? false; + } catch (_) { + return false; + } + } + + /// Receiver: serve [rendezvous] over HCE until [stopPublish]. + static Future publish(TapRendezvous rendezvous) => + _hce.invokeMethod('start', {'payload': _scheme + _encode(rendezvous)}); + + static Future stopPublish() => _hce.invokeMethod('stop'); + + /// Receiver: fires when a reader actually pulls the published rendezvous, i.e. + /// the instant a tap happens. The receiver uses this to start scanning for the + /// sender's advertisement; scanning continuously would cost too much battery. + /// May fire more than once per tap, so listeners must be idempotent. + static Stream tagReads() => + _hceEvents.receiveBroadcastStream().map((_) {}); + + /// Sender: enter NFC reader mode. Reads arrive on [reads]. + static Future startReader() => _reader.invokeMethod('startReader'); + + static Future stopReader() => _reader.invokeMethod('stopReader'); + + /// Sender: rendezvous decoded from a tapped receiver. Malformed reads are + /// dropped rather than surfaced. + static Stream reads() => + _readerEvents + .receiveBroadcastStream() + .map((e) => _parseEvent(e as Map)) + .where((r) => r != null) + .cast(); + + /// A fresh random rendezvous UUID for one transfer. + static String randomUuid() { + final rng = Random.secure(); + final b = Uint8List.fromList( + List.generate(16, (_) => rng.nextInt(256)), + ); + b[6] = (b[6] & 0x0f) | 0x40; // version 4 + b[8] = (b[8] & 0x3f) | 0x80; // variant + return _bytesToUuid(b); + } + + static TapRendezvous? _parseEvent(Map map) { + if (map['event'] != 'read') return null; + final uri = map['uri'] as String?; + if (uri == null || !uri.startsWith(_scheme)) return null; + return _decode(uri.substring(_scheme.length)); + } + + static String _encode(TapRendezvous r) { + final out = BytesBuilder(); + out.addByte(_version); + out.add(r.pubkey); + out.add(_uuidToBytes(r.uuid)); + return base64Url.encode(out.toBytes()).replaceAll('=', ''); + } + + static TapRendezvous? _decode(String b64) { + try { + final bytes = base64Url.decode(base64Url.normalize(b64)); + if (bytes.length != _rendezvousLen || bytes[0] != _version) return null; + return TapRendezvous( + pubkey: Uint8List.fromList(bytes.sublist(1, 34)), + uuid: _bytesToUuid(bytes.sublist(34, 50)), + ); + } catch (_) { + return null; + } + } + + static Uint8List _uuidToBytes(String uuid) { + final hex = uuid.replaceAll('-', ''); + final bytes = Uint8List(16); + for (var i = 0; i < 16; i++) { + bytes[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16); + } + return bytes; + } + + static String _bytesToUuid(List b) { + final hex = b.map((x) => x.toRadixString(16).padLeft(2, '0')).join(); + return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-' + '${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}'; + } +} diff --git a/lib/tap_transfer/tap_receive.dart b/lib/tap_transfer/tap_receive.dart new file mode 100644 index 00000000..d0def8b0 --- /dev/null +++ b/lib/tap_transfer/tap_receive.dart @@ -0,0 +1,255 @@ +import 'dart:async'; + +import 'package:ecashapp/extensions/build_context_l10n.dart'; +import 'package:ecashapp/lib.dart'; +import 'package:ecashapp/redeem_ecash.dart'; +import 'package:ecashapp/screens/federation_info_screen.dart'; +import 'package:ecashapp/tap_transfer/ble_tap.dart'; +import 'package:ecashapp/tap_transfer/tap_nfc.dart'; +import 'package:ecashapp/tap_transfer.dart'; +import 'package:ecashapp/theme.dart'; +import 'package:ecashapp/toast.dart'; +import 'package:ecashapp/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +/// App-global passive receiver for "tap to send" ecash (Phase 4b). +/// +/// While armed — app foregrounded, BLE permissions already granted, and no +/// send/invoice screen holding NFC/BLE — it keeps a fresh rendezvous published +/// over NFC (HCE) and advertised over BLE, listening for an incoming encrypted +/// blob. On receipt it decrypts and hands the ecash string to [onEcash] (wired +/// in app.dart to the redeem bottom sheet). Nothing is auto-reissued. +/// +/// Arm/disarm follow the app lifecycle; [pause]/[resume] are a nesting-safe way +/// for screens that need exclusive NFC/BLE (the send screen, the Lightning +/// invoice HCE) to temporarily take over. +class TapReceive { + TapReceive._(); + static final TapReceive instance = TapReceive._(); + + /// Invoked with the decrypted ecash string. Set by app.dart. + void Function(String ecash)? onEcash; + + TapRecipient? _recipient; + StreamSubscription? _sub; + StreamSubscription? _tagSub; + bool _armed = false; + int _pauseCount = 0; + + /// UUID of the rendezvous currently published over NFC. + String? _uuid; + + /// Whether a scan for the sender's advertisement is already in flight, so a + /// tag read arriving twice for one tap doesn't restart it. + bool _scanning = false; + + bool get _paused => _pauseCount > 0; + + /// Arm if possible. No-op if already armed, paused, unsupported, or if BLE + /// permissions aren't already granted — we never prompt from here, so the + /// feature switches on silently once the user has granted them (e.g. via a + /// tap-send). + Future arm() async { + if (_armed || _paused) return; + try { + if (!await getTapReceiveEnabled()) return; + if (!await BleTap.isAvailable()) return; + if (!await TapNfc.hceAvailable()) return; + if (!await _permissionsGranted()) return; + _armed = true; + _sub ??= BleTap.events().listen( + _onEvent, + onError: (e) => AppLogger.instance.warn("tap receive stream: $e"), + ); + _tagSub ??= TapNfc.tagReads().listen( + (_) => _onTagRead(), + onError: (e) => AppLogger.instance.warn("tap receive hce stream: $e"), + ); + await _rotate(); + AppLogger.instance.info("tap receive: armed"); + } catch (e) { + _armed = false; + AppLogger.instance.warn("tap receive: arm failed: $e"); + } + } + + Future disarm() async { + if (!_armed) return; + _armed = false; + try { + await _sub?.cancel(); + _sub = null; + await _tagSub?.cancel(); + _tagSub = null; + await TapNfc.stopPublish(); + await BleTap.stop(); + } catch (e) { + AppLogger.instance.warn("tap receive: disarm error: $e"); + } + _recipient?.dispose(); + _recipient = null; + _uuid = null; + _scanning = false; + AppLogger.instance.info("tap receive: disarmed"); + } + + Future pause() async { + _pauseCount++; + await disarm(); + } + + Future resume() async { + if (_pauseCount > 0) _pauseCount--; + if (_pauseCount == 0) await arm(); + } + + Future _rotate() async { + _recipient?.dispose(); + final recipient = TapRecipient(); + _recipient = recipient; + final uuid = TapNfc.randomUuid(); + _uuid = uuid; + _scanning = false; + await BleTap.stop(); + await TapNfc.publish( + TapRendezvous(pubkey: recipient.publicKey(), uuid: uuid), + ); + // No BLE yet: the sender is the peripheral now, so there is nothing to scan + // for until it has read this rendezvous and started advertising it. That + // moment arrives as a tag read - see [_onTagRead]. + } + + /// A reader just pulled our rendezvous off the NFC tag, so the sender is about + /// to advertise it. Start scanning for it. + Future _onTagRead() async { + if (!_armed || _paused || _scanning) return; + final uuid = _uuid; + if (uuid == null) return; + _scanning = true; + AppLogger.instance.info("tap receive: tag read, scanning for sender"); + try { + await BleTap.startReceiving(uuid); + } catch (e) { + _scanning = false; + AppLogger.instance.warn("tap receive: could not start scan: $e"); + } + } + + void _onEvent(BleTapEvent e) { + if (e.event == 'error') { + // Previously dropped, which made every transport failure look like silence. + _scanning = false; + AppLogger.instance.warn("tap receive: ${e.message}"); + return; + } + if (e.event != 'received' || e.data == null) return; + _scanning = false; + final recipient = _recipient; + if (recipient == null) return; + String ecash; + try { + ecash = recipient.decrypt(blob: e.data!); + } catch (err) { + AppLogger.instance.warn("tap receive: decrypt failed: $err"); + _rotate(); + return; + } + AppLogger.instance.info("tap receive: decrypted a token, presenting"); + onEcash?.call(ecash); + // Fresh rendezvous + key for the next transfer. + _rotate(); + } + + Future _permissionsGranted() async { + final scan = await Permission.bluetoothScan.status; + final connect = await Permission.bluetoothConnect.status; + final advertise = await Permission.bluetoothAdvertise.status; + return scan.isGranted && connect.isGranted && advertise.isGranted; + } + + /// Prompt for the BLE permissions the receiver needs. Called from the Settings + /// toggle (the one place we're allowed to prompt), not from [arm]. + Future requestPermissions() async { + final statuses = + await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.bluetoothAdvertise, + ].request(); + return statuses.values.every((s) => s.isGranted); + } +} + +/// Route a received ecash string through the same path the QR scanner uses: the +/// redeem bottom sheet for a known federation, the join flow for an invite-code +/// ecash, or an error for ecash with no resolvable federation. +Future presentReceivedEcash(BuildContext context, String ecash) async { + try { + final (action, fed) = await parsedScannedText(text: ecash); + if (!context.mounted) return; + switch (action) { + case ParsedText_Ecash(:final field0): + if (fed == null) return; + await showAppModalBottomSheet( + context: context, + heightFactor: 0.5, + childBuilder: + () async => + EcashRedeemPrompt(fed: fed, ecash: ecash, amount: field0), + ); + break; + case ParsedText_InviteCodeWithEcash(:final field0, :final field1): + await _presentJoin(context, field0, field1); + break; + case ParsedText_EcashNoFederation(): + ToastService().show( + message: context.l10n.validEcashNoFederation, + duration: const Duration(seconds: 5), + onTap: () {}, + icon: const Icon(Icons.error), + ); + break; + default: + break; + } + } catch (e) { + AppLogger.instance.warn("tap receive: could not present ecash: $e"); + } +} + +Future _presentJoin( + BuildContext context, + String inviteCode, + String ecash, +) async { + try { + final meta = await getFederationMeta(inviteCode: inviteCode); + if (!context.mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: + (_) => FederationInfoScreen( + fed: meta.selector, + inviteCode: inviteCode, + welcomeMessage: meta.welcome, + imageUrl: meta.picture, + joinable: true, + ecash: ecash, + onLeaveFederation: () {}, + ), + ), + ); + } catch (e) { + AppLogger.instance.warn("tap receive: federation meta failed: $e"); + if (context.mounted) { + ToastService().show( + message: context.l10n.couldNotGetFederationMetadataScan, + duration: const Duration(seconds: 5), + onTap: () {}, + icon: const Icon(Icons.error), + ); + } + } +} diff --git a/lib/tap_transfer/tap_transfer_dev_screen.dart b/lib/tap_transfer/tap_transfer_dev_screen.dart new file mode 100644 index 00000000..4361ccc7 --- /dev/null +++ b/lib/tap_transfer/tap_transfer_dev_screen.dart @@ -0,0 +1,291 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:ecashapp/lib.dart'; +import 'package:ecashapp/tap_transfer/ble_tap.dart'; +import 'package:ecashapp/tap_transfer/tap_nfc.dart'; +import 'package:ecashapp/tap_transfer/tap_receive.dart'; +import 'package:ecashapp/tap_transfer.dart'; +import 'package:ecashapp/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +/// Debug-only harness for Phase 3 of the NFC + BLE "tap to send" feature. +/// +/// Full end-to-end flow: the receiver generates an ephemeral key + rendezvous +/// UUID, publishes them over NFC (HCE) and advertises over BLE; the sender enters +/// reader mode, taps, reads the rendezvous, encrypts with [encryptEcashForTap] +/// for the NFC-delivered pubkey, and streams the blob over BLE. The receiver +/// decrypts with [TapRecipient]. Any text works as the payload — this verifies +/// the handshake + transport + crypto, not reissue (that's Phase 4). +class TapTransferDevScreen extends StatefulWidget { + const TapTransferDevScreen({super.key}); + + @override + State createState() => _TapTransferDevScreenState(); +} + +enum _Mode { idle, receiving, sending } + +class _TapTransferDevScreenState extends State { + final TextEditingController _payloadController = TextEditingController( + text: + 'fed1-tap-transfer-dev-payload-${DateTime.now().millisecondsSinceEpoch}', + ); + final List _log = []; + + StreamSubscription? _bleSub; + StreamSubscription? _nfcSub; + StreamSubscription? _tagSub; + String? _rxUuid; + bool _rxScanning = false; + TapRecipient? _recipient; + _Mode _mode = _Mode.idle; + bool _sendStarted = false; + String? _result; + + @override + void initState() { + super.initState(); + TapReceive.instance.pause(); + _bleSub = BleTap.events().listen( + _onBleEvent, + onError: (e) => _append('ble stream error: $e'), + ); + _nfcSub = TapNfc.reads().listen( + _onRendezvous, + onError: (e) => _append('nfc stream error: $e'), + ); + _tagSub = TapNfc.tagReads().listen( + (_) => _onTagRead(), + onError: (e) => _append('hce stream error: $e'), + ); + } + + /// Receiving side: our tag was read, so the sender is about to advertise the + /// rendezvous. Scan for it. + Future _onTagRead() async { + if (_mode != _Mode.receiving || _rxScanning) return; + final uuid = _rxUuid; + if (uuid == null) return; + _rxScanning = true; + _append('tag read, scanning for sender…'); + await BleTap.startReceiving(uuid); + } + + @override + void dispose() { + _bleSub?.cancel(); + _nfcSub?.cancel(); + _tagSub?.cancel(); + BleTap.stop(); + TapNfc.stopPublish(); + TapNfc.stopReader(); + _recipient?.dispose(); + _payloadController.dispose(); + TapReceive.instance.resume(); + super.dispose(); + } + + void _append(String line) { + if (!mounted) return; + setState(() => _log.insert(0, line)); + } + + void _onBleEvent(BleTapEvent e) { + _append(e.toString()); + AppLogger.instance.info("tap ble(rx): $e"); + switch (e.event) { + case 'received': + if (_mode == _Mode.receiving && e.data != null) { + _decryptReceived(e.data!); + } + break; + case 'status': + if (e.state == 'sent' || e.state == 'confirmed') { + setState(() => _result = 'Sent (${e.state})'); + } + break; + case 'error': + setState(() => _result = 'Error: ${e.message}'); + break; + } + } + + void _onRendezvous(TapRendezvous r) { + if (_mode != _Mode.sending || _sendStarted) return; + _sendStarted = true; + _append('tapped: uuid=${r.uuid} pubkey=${r.pubkey.length}B'); + TapNfc.stopReader(); + try { + final blob = encryptEcashForTap( + ecash: _payloadController.text, + recipientPubkey: r.pubkey, + ); + _append('encrypted ${blob.length}B, advertising over BLE…'); + BleTap.startSending(r.uuid, blob); + } catch (e) { + setState(() => _result = 'Encrypt failed: $e'); + } + } + + void _decryptReceived(Uint8List blob) { + final recipient = _recipient; + if (recipient == null) return; + try { + final text = recipient.decrypt(blob: blob); + setState(() => _result = 'Received & decrypted:\n$text'); + } catch (e) { + setState(() => _result = 'Decrypt failed: $e'); + } + } + + Future _ensurePermissions() async { + final statuses = + await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.bluetoothAdvertise, + ].request(); + final granted = statuses.values.every((s) => s.isGranted); + if (!granted) _append('permissions denied: $statuses'); + return granted; + } + + Future _startReceive() async { + if (!await BleTap.isAvailable()) { + setState(() => _result = 'BLE unavailable (off or unsupported)'); + return; + } + if (!await TapNfc.hceAvailable()) { + setState(() => _result = 'NFC/HCE unavailable (off or unsupported)'); + return; + } + if (!await _ensurePermissions()) return; + + final recipient = TapRecipient(); + _recipient?.dispose(); + _recipient = recipient; + final pubkey = recipient.publicKey(); + final uuid = TapNfc.randomUuid(); + setState(() { + _mode = _Mode.receiving; + _result = 'Waiting for a tap…'; + }); + _append('rendezvous uuid=$uuid pubkey=${pubkey.length}B'); + _rxUuid = uuid; + _rxScanning = false; + await TapNfc.publish(TapRendezvous(pubkey: pubkey, uuid: uuid)); + // BLE starts on the tag read - the sender is the peripheral now. + } + + Future _startSend() async { + if (!await BleTap.isAvailable()) { + setState(() => _result = 'BLE unavailable (off or unsupported)'); + return; + } + if (!await _ensurePermissions()) return; + _sendStarted = false; + setState(() { + _mode = _Mode.sending; + _result = 'Tap the receiver…'; + }); + await TapNfc.startReader(); + } + + Future _stop() async { + await BleTap.stop(); + await TapNfc.stopPublish(); + await TapNfc.stopReader(); + _recipient?.dispose(); + _recipient = null; + _sendStarted = false; + setState(() { + _mode = _Mode.idle; + _result = null; + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final busy = _mode != _Mode.idle; + + return Scaffold( + appBar: AppBar(title: const Text('Tap transfer (dev)')), // i18n-ignore + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + controller: _payloadController, + enabled: !busy, + maxLines: 2, + decoration: const InputDecoration( + labelText: 'Payload to send', // i18n-ignore + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: FilledButton.icon( + onPressed: busy ? null : _startReceive, + icon: const Icon(Icons.download), + label: const Text('Receive'), // i18n-ignore + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton.icon( + onPressed: busy ? null : _startSend, + icon: const Icon(Icons.upload), + label: const Text('Send'), // i18n-ignore + ), + ), + ], + ), + const SizedBox(height: 8), + OutlinedButton.icon( + onPressed: busy ? _stop : null, + icon: const Icon(Icons.stop), + label: const Text('Stop'), // i18n-ignore + ), + const SizedBox(height: 16), + if (_result != null) + Card( + color: theme.colorScheme.surfaceContainerHighest, + child: Padding( + padding: const EdgeInsets.all(12), + child: SelectableText( + _result!, + style: theme.textTheme.bodyMedium, + ), + ), + ), + const SizedBox(height: 16), + Text('Event log', style: theme.textTheme.titleSmall), // i18n-ignore + const Divider(), + Expanded( + child: ListView.builder( + itemCount: _log.length, + itemBuilder: + (_, i) => Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Text( + _log[i], + style: theme.textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/rust/ecashapp/Cargo.lock b/rust/ecashapp/Cargo.lock index 2fe1666b..ca5cd8c3 100644 --- a/rust/ecashapp/Cargo.lock +++ b/rust/ecashapp/Cargo.lock @@ -1529,6 +1529,7 @@ dependencies = [ "async-trait", "bitcoin", "bitcoin-payment-instructions", + "chacha20poly1305", "fedimint-api-client", "fedimint-bip39", "fedimint-client", @@ -1555,6 +1556,7 @@ dependencies = [ "futures-timer", "futures-util", "hex", + "hkdf", "jni 0.21.1", "lightning-invoice", "lnurl-rs", @@ -1565,6 +1567,7 @@ dependencies = [ "semver", "serde", "serde_json", + "sha2 0.10.9", "subtle", "thiserror 1.0.69", "tokio", @@ -3231,6 +3234,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" diff --git a/rust/ecashapp/Cargo.toml b/rust/ecashapp/Cargo.toml index 9779c3e2..949cd62a 100644 --- a/rust/ecashapp/Cargo.toml +++ b/rust/ecashapp/Cargo.toml @@ -12,6 +12,11 @@ argon2 = "0.5.3" async-stream = "0.3.6" async-trait = "0.1.88" bitcoin = { version = "0.32.5", features = ["serde"] } +# Tap-to-send ecash (NFC + BLE): ECIES key derivation and AEAD. secp256k1 (for +# the ECDH half) comes from the `bitcoin` crate above. See src/tap_transfer.rs. +chacha20poly1305 = "0.10" +hkdf = "0.12" +sha2 = "0.10" bitcoin-payment-instructions = { version = "0.4.0", features = [ "http" ]} fedimint-api-client = { git = "https://github.com/fedimint/fedimint", rev = "3dfb8f5baeb6dcf6cbd268167aae2a2c78b36742" } fedimint-bip39 = { git = "https://github.com/fedimint/fedimint", rev = "3dfb8f5baeb6dcf6cbd268167aae2a2c78b36742" } diff --git a/rust/ecashapp/src/db.rs b/rust/ecashapp/src/db.rs index cb0727ea..897ec256 100644 --- a/rust/ecashapp/src/db.rs +++ b/rust/ecashapp/src/db.rs @@ -103,6 +103,7 @@ pub(crate) enum DbKeyPrefix { PinAttempts = 0x18, NwcLimits = 0x19, NwcSpendWindow = 0x1A, + TapReceiveEnabled = 0x1B, } #[derive(Debug, Clone, Encodable, Decodable, Eq, PartialEq, Hash, Ord, PartialOrd)] @@ -559,6 +560,15 @@ impl_db_record!( db_prefix = DbKeyPrefix::ShowMsats, ); +#[derive(Debug, Encodable, Decodable)] +pub(crate) struct TapReceiveEnabledKey; + +impl_db_record!( + key = TapReceiveEnabledKey, + value = (), + db_prefix = DbKeyPrefix::TapReceiveEnabled, +); + /// Tracks every walletv2 receive (peg-in) address we have handed out, which is /// the source of truth for the deposit-address list shown in the UI. Unlike /// walletv1, walletv2 creates no client operation at address-allocation time diff --git a/rust/ecashapp/src/frb_generated.rs b/rust/ecashapp/src/frb_generated.rs index 64e54d6a..3b937a90 100644 --- a/rust/ecashapp/src/frb_generated.rs +++ b/rust/ecashapp/src/frb_generated.rs @@ -30,6 +30,7 @@ use crate::event_bus::*; use crate::fountain::*; use crate::multimint::*; use crate::nostr::*; +use crate::tap_transfer::*; use crate::*; use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; @@ -43,7 +44,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.9.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1098215331; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 586808380; // Section: executor @@ -4431,6 +4432,64 @@ fn wire__crate__multimint__Multimint_get_show_msats_impl( }, ) } +fn wire__crate__multimint__Multimint_get_tap_receive_enabled_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "Multimint_get_tap_receive_enabled", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok( + crate::multimint::Multimint::get_tap_receive_enabled(&*api_that_guard) + .await, + )?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__multimint__Multimint_guardian_add_gateway_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -6842,6 +6901,68 @@ fn wire__crate__multimint__Multimint_set_show_msats_impl( }, ) } +fn wire__crate__multimint__Multimint_set_tap_receive_enabled_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "Multimint_set_tap_receive_enabled", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_enabled = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok({ + crate::multimint::Multimint::set_tap_receive_enabled( + &*api_that_guard, + api_enabled, + ) + .await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__multimint__Multimint_transactions_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -9334,6 +9455,131 @@ fn wire__crate__nostr__PublicFederation_auto_accessor_set_picture_impl( }, ) } +fn wire__crate__tap_transfer__TapRecipient_decrypt_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "TapRecipient_decrypt", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_blob = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, crate::app_error::EcashAppError>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + crate::tap_transfer::TapRecipient::decrypt(&*api_that_guard, api_blob)?; + Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__tap_transfer__TapRecipient_new_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "TapRecipient_new", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::tap_transfer::TapRecipient::new())?; + Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__tap_transfer__TapRecipient_public_key_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "TapRecipient_public_key", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok( + crate::tap_transfer::TapRecipient::public_key(&*api_that_guard), + )?; + Ok(output_ok) + })()) + }, + ) +} fn wire__crate__multimint__WithdrawFeesResponse_auto_accessor_get_federation_fee_msats_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -10931,6 +11177,37 @@ fn wire__crate__create_new_multimint_impl( }, ) } +fn wire__crate__encrypt_ecash_for_tap_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "encrypt_ecash_for_tap", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_ecash = ::sse_decode(&mut deserializer); + let api_recipient_pubkey = >::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, crate::app_error::EcashAppError>((move || { + let output_ok = crate::encrypt_ecash_for_tap(api_ecash, api_recipient_pubkey)?; + Ok(output_ok) + })()) + }, + ) +} fn wire__crate__execute_lnurl_withdraw_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -12106,6 +12383,42 @@ fn wire__crate__get_show_msats_impl( }, ) } +fn wire__crate__get_tap_receive_enabled_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_tap_receive_enabled", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = + Result::<_, ()>::Ok(crate::get_tap_receive_enabled().await)?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__guardian_add_gateway_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -14607,6 +14920,44 @@ fn wire__crate__set_show_msats_impl( }, ) } +fn wire__crate__set_tap_receive_enabled_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_tap_receive_enabled", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_enabled = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let output_ok = Result::<_, ()>::Ok({ + crate::set_tap_receive_enabled(api_enabled).await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__subscribe_deposits_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -15191,6 +15542,9 @@ flutter_rust_bridge::frb_generated_moi_arc_impl_value!( flutter_rust_bridge::frb_generated_moi_arc_impl_value!( flutter_rust_bridge::for_generated::RustAutoOpaqueInner ); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); flutter_rust_bridge::frb_generated_moi_arc_impl_value!( flutter_rust_bridge::for_generated::RustAutoOpaqueInner ); @@ -15492,6 +15846,16 @@ impl SseDecode for SafeUrl { } } +impl SseDecode for TapRecipient { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + impl SseDecode for WithdrawFees { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -15818,6 +16182,16 @@ impl SseDecode for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + impl SseDecode for RustOpaqueMoi> { @@ -17835,390 +18209,404 @@ fn pde_ffi_dispatcher_primary_impl( 75 => { wire__crate__multimint__Multimint_get_show_msats_impl(port, ptr, rust_vec_len, data_len) } - 76 => wire__crate__multimint__Multimint_guardian_add_gateway_impl( + 76 => wire__crate__multimint__Multimint_get_tap_receive_enabled_impl( port, ptr, rust_vec_len, data_len, ), - 77 => { + 77 => wire__crate__multimint__Multimint_guardian_add_gateway_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 78 => { wire__crate__multimint__Multimint_guardian_audit_impl(port, ptr, rust_vec_len, data_len) } - 78 => wire__crate__multimint__Multimint_guardian_backup_statistics_impl( + 79 => wire__crate__multimint__Multimint_guardian_backup_statistics_impl( port, ptr, rust_vec_len, data_len, ), - 79 => wire__crate__multimint__Multimint_guardian_list_gateways_impl( + 80 => wire__crate__multimint__Multimint_guardian_list_gateways_impl( port, ptr, rust_vec_len, data_len, ), - 80 => { + 81 => { wire__crate__multimint__Multimint_guardian_login_impl(port, ptr, rust_vec_len, data_len) } - 81 => wire__crate__multimint__Multimint_guardian_meta_accept_impl( + 82 => wire__crate__multimint__Multimint_guardian_meta_accept_impl( port, ptr, rust_vec_len, data_len, ), - 82 => wire__crate__multimint__Multimint_guardian_meta_propose_field_impl( + 83 => wire__crate__multimint__Multimint_guardian_meta_propose_field_impl( port, ptr, rust_vec_len, data_len, ), - 83 => wire__crate__multimint__Multimint_guardian_meta_state_impl( + 84 => wire__crate__multimint__Multimint_guardian_meta_state_impl( port, ptr, rust_vec_len, data_len, ), - 84 => wire__crate__multimint__Multimint_guardian_meta_withdraw_impl( + 85 => wire__crate__multimint__Multimint_guardian_meta_withdraw_impl( port, ptr, rust_vec_len, data_len, ), - 85 => wire__crate__multimint__Multimint_guardian_remove_gateway_impl( + 86 => wire__crate__multimint__Multimint_guardian_remove_gateway_impl( port, ptr, rust_vec_len, data_len, ), - 86 => wire__crate__multimint__Multimint_guardian_status_impl( + 87 => wire__crate__multimint__Multimint_guardian_status_impl( port, ptr, rust_vec_len, data_len, ), - 87 => wire__crate__multimint__Multimint_has_seed_phrase_ack_impl( + 88 => wire__crate__multimint__Multimint_has_seed_phrase_ack_impl( port, ptr, rust_vec_len, data_len, ), - 88 => wire__crate__multimint__Multimint_join_federation_impl( + 89 => wire__crate__multimint__Multimint_join_federation_impl( port, ptr, rust_vec_len, data_len, ), - 89 => wire__crate__multimint__Multimint_leave_federation_impl( + 90 => wire__crate__multimint__Multimint_leave_federation_impl( port, ptr, rust_vec_len, data_len, ), - 90 => wire__crate__multimint__Multimint_max_lightning_send_impl( + 91 => wire__crate__multimint__Multimint_max_lightning_send_impl( port, ptr, rust_vec_len, data_len, ), - 91 => wire__crate__multimint__Multimint_new_impl(port, ptr, rust_vec_len, data_len), - 92 => wire__crate__multimint__Multimint_parse_ecash_impl(port, ptr, rust_vec_len, data_len), - 93 => wire__crate__multimint__Multimint_probe_invoice_is_loopback_impl( + 92 => wire__crate__multimint__Multimint_new_impl(port, ptr, rust_vec_len, data_len), + 93 => wire__crate__multimint__Multimint_parse_ecash_impl(port, ptr, rust_vec_len, data_len), + 94 => wire__crate__multimint__Multimint_probe_invoice_is_loopback_impl( port, ptr, rust_vec_len, data_len, ), - 94 => wire__crate__multimint__Multimint_receive_impl(port, ptr, rust_vec_len, data_len), - 95 => wire__crate__multimint__Multimint_recheck_address_impl( + 95 => wire__crate__multimint__Multimint_receive_impl(port, ptr, rust_vec_len, data_len), + 96 => wire__crate__multimint__Multimint_recheck_address_impl( port, ptr, rust_vec_len, data_len, ), - 96 => wire__crate__multimint__Multimint_recover_ln_address_impl( + 97 => wire__crate__multimint__Multimint_recover_ln_address_impl( port, ptr, rust_vec_len, data_len, ), - 97 => wire__crate__multimint__Multimint_refresh_connections_impl( + 98 => wire__crate__multimint__Multimint_refresh_connections_impl( port, ptr, rust_vec_len, data_len, ), - 98 => wire__crate__multimint__Multimint_refresh_federation_meta_impl( + 99 => wire__crate__multimint__Multimint_refresh_federation_meta_impl( port, ptr, rust_vec_len, data_len, ), - 99 => wire__crate__multimint__Multimint_register_ln_address_impl( + 100 => wire__crate__multimint__Multimint_register_ln_address_impl( port, ptr, rust_vec_len, data_len, ), - 100 => { + 101 => { wire__crate__multimint__Multimint_reissue_ecash_impl(port, ptr, rust_vec_len, data_len) } - 101 => wire__crate__multimint__Multimint_rejoin_from_backup_invites_impl( + 102 => wire__crate__multimint__Multimint_rejoin_from_backup_invites_impl( port, ptr, rust_vec_len, data_len, ), - 102 => wire__crate__multimint__Multimint_send_impl(port, ptr, rust_vec_len, data_len), - 103 => wire__crate__multimint__Multimint_send_ecash_impl(port, ptr, rust_vec_len, data_len), - 104 => wire__crate__multimint__Multimint_set_bitcoin_display_impl( + 103 => wire__crate__multimint__Multimint_send_impl(port, ptr, rust_vec_len, data_len), + 104 => wire__crate__multimint__Multimint_send_ecash_impl(port, ptr, rust_vec_len, data_len), + 105 => wire__crate__multimint__Multimint_set_bitcoin_display_impl( port, ptr, rust_vec_len, data_len, ), - 105 => wire__crate__multimint__Multimint_set_federation_order_impl( + 106 => wire__crate__multimint__Multimint_set_federation_order_impl( port, ptr, rust_vec_len, data_len, ), - 106 => wire__crate__multimint__Multimint_set_fiat_currency_impl( + 107 => wire__crate__multimint__Multimint_set_fiat_currency_impl( port, ptr, rust_vec_len, data_len, ), - 107 => { + 108 => { wire__crate__multimint__Multimint_set_show_msats_impl(port, ptr, rust_vec_len, data_len) } - 108 => { + 109 => wire__crate__multimint__Multimint_set_tap_receive_enabled_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 110 => { wire__crate__multimint__Multimint_transactions_impl(port, ptr, rust_vec_len, data_len) } - 109 => { + 111 => { wire__crate__multimint__Multimint_wallet_summary_impl(port, ptr, rust_vec_len, data_len) } - 110 => wire__crate__multimint__Multimint_withdraw_to_address_impl( + 112 => wire__crate__multimint__Multimint_withdraw_to_address_impl( port, ptr, rust_vec_len, data_len, ), - 111 => wire__crate__nostr__NostrClient_backup_invite_codes_impl( + 113 => wire__crate__nostr__NostrClient_backup_invite_codes_impl( port, ptr, rust_vec_len, data_len, ), - 112 => wire__crate__nostr__NostrClient_clear_contacts_and_stop_sync_impl( + 114 => wire__crate__nostr__NostrClient_clear_contacts_and_stop_sync_impl( port, ptr, rust_vec_len, data_len, ), - 113 => wire__crate__nostr__NostrClient_fetch_nostr_profiles_impl( + 115 => wire__crate__nostr__NostrClient_fetch_nostr_profiles_impl( port, ptr, rust_vec_len, data_len, ), - 114 => { + 116 => { wire__crate__nostr__NostrClient_get_all_contacts_impl(port, ptr, rust_vec_len, data_len) } - 115 => wire__crate__nostr__NostrClient_get_backup_invite_codes_impl( + 117 => wire__crate__nostr__NostrClient_get_backup_invite_codes_impl( port, ptr, rust_vec_len, data_len, ), - 116 => wire__crate__nostr__NostrClient_get_contact_impl(port, ptr, rust_vec_len, data_len), - 117 => wire__crate__nostr__NostrClient_get_contact_sync_config_impl( + 118 => wire__crate__nostr__NostrClient_get_contact_impl(port, ptr, rust_vec_len, data_len), + 119 => wire__crate__nostr__NostrClient_get_contact_sync_config_impl( port, ptr, rust_vec_len, data_len, ), - 118 => wire__crate__nostr__NostrClient_get_follows_for_pubkey_impl( + 120 => wire__crate__nostr__NostrClient_get_follows_for_pubkey_impl( port, ptr, rust_vec_len, data_len, ), - 119 => wire__crate__nostr__NostrClient_get_nwc_connection_info_impl( + 121 => wire__crate__nostr__NostrClient_get_nwc_connection_info_impl( port, ptr, rust_vec_len, data_len, ), - 120 => wire__crate__nostr__NostrClient_get_public_federations_impl( + 122 => wire__crate__nostr__NostrClient_get_public_federations_impl( port, ptr, rust_vec_len, data_len, ), - 121 => wire__crate__nostr__NostrClient_get_relays_impl(port, ptr, rust_vec_len, data_len), - 122 => wire__crate__nostr__NostrClient_has_imported_contacts_impl( + 123 => wire__crate__nostr__NostrClient_get_relays_impl(port, ptr, rust_vec_len, data_len), + 124 => wire__crate__nostr__NostrClient_has_imported_contacts_impl( port, ptr, rust_vec_len, data_len, ), - 123 => wire__crate__nostr__NostrClient_insert_relay_impl(port, ptr, rust_vec_len, data_len), - 124 => wire__crate__nostr__NostrClient_new_impl(port, ptr, rust_vec_len, data_len), - 125 => wire__crate__nostr__NostrClient_paginate_contacts_impl( + 125 => wire__crate__nostr__NostrClient_insert_relay_impl(port, ptr, rust_vec_len, data_len), + 126 => wire__crate__nostr__NostrClient_new_impl(port, ptr, rust_vec_len, data_len), + 127 => wire__crate__nostr__NostrClient_paginate_contacts_impl( port, ptr, rust_vec_len, data_len, ), - 126 => wire__crate__nostr__NostrClient_paginate_search_contacts_impl( + 128 => wire__crate__nostr__NostrClient_paginate_search_contacts_impl( port, ptr, rust_vec_len, data_len, ), - 127 => wire__crate__nostr__NostrClient_remove_nwc_connection_info_impl( + 129 => wire__crate__nostr__NostrClient_remove_nwc_connection_info_impl( port, ptr, rust_vec_len, data_len, ), - 128 => wire__crate__nostr__NostrClient_remove_relay_impl(port, ptr, rust_vec_len, data_len), - 129 => wire__crate__nostr__NostrClient_set_contact_sync_config_impl( + 130 => wire__crate__nostr__NostrClient_remove_relay_impl(port, ptr, rust_vec_len, data_len), + 131 => wire__crate__nostr__NostrClient_set_contact_sync_config_impl( port, ptr, rust_vec_len, data_len, ), - 130 => wire__crate__nostr__NostrClient_set_nwc_connection_info_impl( + 132 => wire__crate__nostr__NostrClient_set_nwc_connection_info_impl( port, ptr, rust_vec_len, data_len, ), - 131 => { + 133 => { wire__crate__nostr__NostrClient_sync_contacts_impl(port, ptr, rust_vec_len, data_len) } - 132 => wire__crate__nostr__NostrClient_verify_nip05_impl(port, ptr, rust_vec_len, data_len), - 136 => wire__crate__fountain__OobNotesEncoder_next_fragment_impl( + 134 => wire__crate__nostr__NostrClient_verify_nip05_impl(port, ptr, rust_vec_len, data_len), + 138 => wire__crate__fountain__OobNotesEncoder_next_fragment_impl( port, ptr, rust_vec_len, data_len, ), - 163 => wire__crate__ack_seed_phrase_impl(port, ptr, rust_vec_len, data_len), - 164 => wire__crate__add_recovery_relay_impl(port, ptr, rust_vec_len, data_len), - 165 => wire__crate__allocate_deposit_address_impl(port, ptr, rust_vec_len, data_len), - 166 => wire__crate__await_ecash_reissue_impl(port, ptr, rust_vec_len, data_len), - 167 => wire__crate__await_receive_impl(port, ptr, rust_vec_len, data_len), - 168 => wire__crate__await_send_impl(port, ptr, rust_vec_len, data_len), - 169 => wire__crate__await_withdraw_impl(port, ptr, rust_vec_len, data_len), - 170 => wire__crate__backup_invite_codes_impl(port, ptr, rust_vec_len, data_len), - 171 => wire__crate__balance_impl(port, ptr, rust_vec_len, data_len), - 172 => wire__crate__calculate_ecash_reissue_fees_impl(port, ptr, rust_vec_len, data_len), - 173 => wire__crate__calculate_ecash_send_fees_impl(port, ptr, rust_vec_len, data_len), - 174 => wire__crate__calculate_withdraw_fees_impl(port, ptr, rust_vec_len, data_len), - 175 => wire__crate__change_pin_code_impl(port, ptr, rust_vec_len, data_len), - 176 => wire__crate__check_ecash_spent_impl(port, ptr, rust_vec_len, data_len), - 177 => wire__crate__check_ln_address_availability_impl(port, ptr, rust_vec_len, data_len), - 178 => wire__crate__claim_random_ln_address_impl(port, ptr, rust_vec_len, data_len), - 179 => wire__crate__clear_contacts_and_stop_sync_impl(port, ptr, rust_vec_len, data_len), - 180 => wire__crate__clear_pin_code_impl(port, ptr, rust_vec_len, data_len), - 181 => { + 168 => wire__crate__ack_seed_phrase_impl(port, ptr, rust_vec_len, data_len), + 169 => wire__crate__add_recovery_relay_impl(port, ptr, rust_vec_len, data_len), + 170 => wire__crate__allocate_deposit_address_impl(port, ptr, rust_vec_len, data_len), + 171 => wire__crate__await_ecash_reissue_impl(port, ptr, rust_vec_len, data_len), + 172 => wire__crate__await_receive_impl(port, ptr, rust_vec_len, data_len), + 173 => wire__crate__await_send_impl(port, ptr, rust_vec_len, data_len), + 174 => wire__crate__await_withdraw_impl(port, ptr, rust_vec_len, data_len), + 175 => wire__crate__backup_invite_codes_impl(port, ptr, rust_vec_len, data_len), + 176 => wire__crate__balance_impl(port, ptr, rust_vec_len, data_len), + 177 => wire__crate__calculate_ecash_reissue_fees_impl(port, ptr, rust_vec_len, data_len), + 178 => wire__crate__calculate_ecash_send_fees_impl(port, ptr, rust_vec_len, data_len), + 179 => wire__crate__calculate_withdraw_fees_impl(port, ptr, rust_vec_len, data_len), + 180 => wire__crate__change_pin_code_impl(port, ptr, rust_vec_len, data_len), + 181 => wire__crate__check_ecash_spent_impl(port, ptr, rust_vec_len, data_len), + 182 => wire__crate__check_ln_address_availability_impl(port, ptr, rust_vec_len, data_len), + 183 => wire__crate__claim_random_ln_address_impl(port, ptr, rust_vec_len, data_len), + 184 => wire__crate__clear_contacts_and_stop_sync_impl(port, ptr, rust_vec_len, data_len), + 185 => wire__crate__clear_pin_code_impl(port, ptr, rust_vec_len, data_len), + 186 => { wire__crate__compute_receive_amount_with_fees_impl(port, ptr, rust_vec_len, data_len) } - 182 => wire__crate__db__connector_default_impl(port, ptr, rust_vec_len, data_len), - 183 => wire__crate__create_multimint_from_words_impl(port, ptr, rust_vec_len, data_len), - 184 => wire__crate__create_new_multimint_impl(port, ptr, rust_vec_len, data_len), - 185 => wire__crate__execute_lnurl_withdraw_impl(port, ptr, rust_vec_len, data_len), - 186 => wire__crate__federation_id_to_string_impl(port, ptr, rust_vec_len, data_len), - 187 => wire__crate__federations_impl(port, ptr, rust_vec_len, data_len), - 188 => wire__crate__fetch_lnurl_withdraw_impl(port, ptr, rust_vec_len, data_len), - 189 => wire__crate__get_addresses_impl(port, ptr, rust_vec_len, data_len), - 190 => wire__crate__get_all_btc_prices_impl(port, ptr, rust_vec_len, data_len), - 191 => wire__crate__get_all_contacts_impl(port, ptr, rust_vec_len, data_len), - 192 => wire__crate__get_bitcoin_display_impl(port, ptr, rust_vec_len, data_len), - 193 => wire__crate__get_btc_price_impl(port, ptr, rust_vec_len, data_len), - 194 => wire__crate__get_event_bus_impl(port, ptr, rust_vec_len, data_len), - 195 => wire__crate__get_federation_meta_impl(port, ptr, rust_vec_len, data_len), - 196 => wire__crate__get_federation_order_impl(port, ptr, rust_vec_len, data_len), - 197 => wire__crate__get_fiat_currency_impl(port, ptr, rust_vec_len, data_len), - 198 => wire__crate__get_invite_code_impl(port, ptr, rust_vec_len, data_len), - 199 => { + 187 => wire__crate__db__connector_default_impl(port, ptr, rust_vec_len, data_len), + 188 => wire__crate__create_multimint_from_words_impl(port, ptr, rust_vec_len, data_len), + 189 => wire__crate__create_new_multimint_impl(port, ptr, rust_vec_len, data_len), + 191 => wire__crate__execute_lnurl_withdraw_impl(port, ptr, rust_vec_len, data_len), + 192 => wire__crate__federation_id_to_string_impl(port, ptr, rust_vec_len, data_len), + 193 => wire__crate__federations_impl(port, ptr, rust_vec_len, data_len), + 194 => wire__crate__fetch_lnurl_withdraw_impl(port, ptr, rust_vec_len, data_len), + 195 => wire__crate__get_addresses_impl(port, ptr, rust_vec_len, data_len), + 196 => wire__crate__get_all_btc_prices_impl(port, ptr, rust_vec_len, data_len), + 197 => wire__crate__get_all_contacts_impl(port, ptr, rust_vec_len, data_len), + 198 => wire__crate__get_bitcoin_display_impl(port, ptr, rust_vec_len, data_len), + 199 => wire__crate__get_btc_price_impl(port, ptr, rust_vec_len, data_len), + 200 => wire__crate__get_event_bus_impl(port, ptr, rust_vec_len, data_len), + 201 => wire__crate__get_federation_meta_impl(port, ptr, rust_vec_len, data_len), + 202 => wire__crate__get_federation_order_impl(port, ptr, rust_vec_len, data_len), + 203 => wire__crate__get_fiat_currency_impl(port, ptr, rust_vec_len, data_len), + 204 => wire__crate__get_invite_code_impl(port, ptr, rust_vec_len, data_len), + 205 => { wire__crate__get_invoice_from_lnaddress_or_lnurl_impl(port, ptr, rust_vec_len, data_len) } - 200 => wire__crate__get_ln_address_config_impl(port, ptr, rust_vec_len, data_len), - 201 => wire__crate__get_max_withdrawable_amount_impl(port, ptr, rust_vec_len, data_len), - 202 => wire__crate__get_mnemonic_impl(port, ptr, rust_vec_len, data_len), - 203 => wire__crate__get_module_recovery_progress_impl(port, ptr, rust_vec_len, data_len), - 204 => wire__crate__get_note_summary_impl(port, ptr, rust_vec_len, data_len), - 205 => wire__crate__get_nwc_connection_info_impl(port, ptr, rust_vec_len, data_len), - 206 => wire__crate__get_nwc_daily_budget_sats_impl(port, ptr, rust_vec_len, data_len), - 207 => wire__crate__get_nwc_max_payment_sats_impl(port, ptr, rust_vec_len, data_len), - 208 => wire__crate__get_pegin_fee_quote_impl(port, ptr, rust_vec_len, data_len), - 209 => wire__crate__get_relays_impl(port, ptr, rust_vec_len, data_len), - 210 => wire__crate__get_require_pin_for_spending_impl(port, ptr, rust_vec_len, data_len), - 211 => wire__crate__get_show_msats_impl(port, ptr, rust_vec_len, data_len), - 212 => wire__crate__guardian_add_gateway_impl(port, ptr, rust_vec_len, data_len), - 213 => wire__crate__guardian_audit_impl(port, ptr, rust_vec_len, data_len), - 214 => wire__crate__guardian_backup_statistics_impl(port, ptr, rust_vec_len, data_len), - 215 => wire__crate__guardian_list_gateways_impl(port, ptr, rust_vec_len, data_len), - 216 => wire__crate__guardian_login_impl(port, ptr, rust_vec_len, data_len), - 217 => wire__crate__guardian_meta_accept_impl(port, ptr, rust_vec_len, data_len), - 218 => wire__crate__guardian_meta_propose_field_impl(port, ptr, rust_vec_len, data_len), - 219 => wire__crate__guardian_meta_state_impl(port, ptr, rust_vec_len, data_len), - 220 => wire__crate__guardian_meta_withdraw_impl(port, ptr, rust_vec_len, data_len), - 221 => wire__crate__guardian_remove_gateway_impl(port, ptr, rust_vec_len, data_len), - 222 => wire__crate__guardian_status_impl(port, ptr, rust_vec_len, data_len), - 223 => wire__crate__has_imported_contacts_impl(port, ptr, rust_vec_len, data_len), - 224 => wire__crate__has_pin_code_impl(port, ptr, rust_vec_len, data_len), - 225 => wire__crate__has_seed_phrase_ack_impl(port, ptr, rust_vec_len, data_len), - 226 => wire__crate__insert_relay_impl(port, ptr, rust_vec_len, data_len), - 228 => wire__crate__join_federation_impl(port, ptr, rust_vec_len, data_len), - 229 => wire__crate__leave_federation_impl(port, ptr, rust_vec_len, data_len), - 230 => wire__crate__list_federations_from_nostr_impl(port, ptr, rust_vec_len, data_len), - 231 => wire__crate__list_gateways_impl(port, ptr, rust_vec_len, data_len), - 232 => wire__crate__list_ln_address_domains_impl(port, ptr, rust_vec_len, data_len), - 233 => wire__crate__listen_for_nwc_blocking_impl(port, ptr, rust_vec_len, data_len), - 234 => wire__crate__load_multimint_impl(port, ptr, rust_vec_len, data_len), - 235 => wire__crate__max_lightning_send_impl(port, ptr, rust_vec_len, data_len), - 236 => wire__crate__paginate_contacts_impl(port, ptr, rust_vec_len, data_len), - 237 => wire__crate__paginate_search_contacts_impl(port, ptr, rust_vec_len, data_len), - 239 => { + 206 => wire__crate__get_ln_address_config_impl(port, ptr, rust_vec_len, data_len), + 207 => wire__crate__get_max_withdrawable_amount_impl(port, ptr, rust_vec_len, data_len), + 208 => wire__crate__get_mnemonic_impl(port, ptr, rust_vec_len, data_len), + 209 => wire__crate__get_module_recovery_progress_impl(port, ptr, rust_vec_len, data_len), + 210 => wire__crate__get_note_summary_impl(port, ptr, rust_vec_len, data_len), + 211 => wire__crate__get_nwc_connection_info_impl(port, ptr, rust_vec_len, data_len), + 212 => wire__crate__get_nwc_daily_budget_sats_impl(port, ptr, rust_vec_len, data_len), + 213 => wire__crate__get_nwc_max_payment_sats_impl(port, ptr, rust_vec_len, data_len), + 214 => wire__crate__get_pegin_fee_quote_impl(port, ptr, rust_vec_len, data_len), + 215 => wire__crate__get_relays_impl(port, ptr, rust_vec_len, data_len), + 216 => wire__crate__get_require_pin_for_spending_impl(port, ptr, rust_vec_len, data_len), + 217 => wire__crate__get_show_msats_impl(port, ptr, rust_vec_len, data_len), + 218 => wire__crate__get_tap_receive_enabled_impl(port, ptr, rust_vec_len, data_len), + 219 => wire__crate__guardian_add_gateway_impl(port, ptr, rust_vec_len, data_len), + 220 => wire__crate__guardian_audit_impl(port, ptr, rust_vec_len, data_len), + 221 => wire__crate__guardian_backup_statistics_impl(port, ptr, rust_vec_len, data_len), + 222 => wire__crate__guardian_list_gateways_impl(port, ptr, rust_vec_len, data_len), + 223 => wire__crate__guardian_login_impl(port, ptr, rust_vec_len, data_len), + 224 => wire__crate__guardian_meta_accept_impl(port, ptr, rust_vec_len, data_len), + 225 => wire__crate__guardian_meta_propose_field_impl(port, ptr, rust_vec_len, data_len), + 226 => wire__crate__guardian_meta_state_impl(port, ptr, rust_vec_len, data_len), + 227 => wire__crate__guardian_meta_withdraw_impl(port, ptr, rust_vec_len, data_len), + 228 => wire__crate__guardian_remove_gateway_impl(port, ptr, rust_vec_len, data_len), + 229 => wire__crate__guardian_status_impl(port, ptr, rust_vec_len, data_len), + 230 => wire__crate__has_imported_contacts_impl(port, ptr, rust_vec_len, data_len), + 231 => wire__crate__has_pin_code_impl(port, ptr, rust_vec_len, data_len), + 232 => wire__crate__has_seed_phrase_ack_impl(port, ptr, rust_vec_len, data_len), + 233 => wire__crate__insert_relay_impl(port, ptr, rust_vec_len, data_len), + 235 => wire__crate__join_federation_impl(port, ptr, rust_vec_len, data_len), + 236 => wire__crate__leave_federation_impl(port, ptr, rust_vec_len, data_len), + 237 => wire__crate__list_federations_from_nostr_impl(port, ptr, rust_vec_len, data_len), + 238 => wire__crate__list_gateways_impl(port, ptr, rust_vec_len, data_len), + 239 => wire__crate__list_ln_address_domains_impl(port, ptr, rust_vec_len, data_len), + 240 => wire__crate__listen_for_nwc_blocking_impl(port, ptr, rust_vec_len, data_len), + 241 => wire__crate__load_multimint_impl(port, ptr, rust_vec_len, data_len), + 242 => wire__crate__max_lightning_send_impl(port, ptr, rust_vec_len, data_len), + 243 => wire__crate__paginate_contacts_impl(port, ptr, rust_vec_len, data_len), + 244 => wire__crate__paginate_search_contacts_impl(port, ptr, rust_vec_len, data_len), + 246 => { wire__crate__parse_scanned_text_for_federation_impl(port, ptr, rust_vec_len, data_len) } - 240 => wire__crate__parsed_scanned_text_impl(port, ptr, rust_vec_len, data_len), - 241 => wire__crate__payment_preview_with_gateways_impl(port, ptr, rust_vec_len, data_len), - 242 => wire__crate__pin_lockout_seconds_impl(port, ptr, rust_vec_len, data_len), - 243 => wire__crate__receive_impl(port, ptr, rust_vec_len, data_len), - 244 => wire__crate__recheck_address_impl(port, ptr, rust_vec_len, data_len), - 245 => wire__crate__refresh_connections_impl(port, ptr, rust_vec_len, data_len), - 246 => wire__crate__refresh_federation_meta_impl(port, ptr, rust_vec_len, data_len), - 247 => wire__crate__register_ln_address_impl(port, ptr, rust_vec_len, data_len), - 248 => wire__crate__reissue_ecash_impl(port, ptr, rust_vec_len, data_len), - 249 => wire__crate__rejoin_from_backup_invites_impl(port, ptr, rust_vec_len, data_len), - 250 => wire__crate__remove_nwc_connection_info_impl(port, ptr, rust_vec_len, data_len), - 251 => wire__crate__remove_relay_impl(port, ptr, rust_vec_len, data_len), - 252 => wire__crate__send_impl(port, ptr, rust_vec_len, data_len), - 253 => wire__crate__send_ecash_impl(port, ptr, rust_vec_len, data_len), - 254 => wire__crate__set_bitcoin_display_impl(port, ptr, rust_vec_len, data_len), - 255 => wire__crate__set_federation_order_impl(port, ptr, rust_vec_len, data_len), - 256 => wire__crate__set_fiat_currency_impl(port, ptr, rust_vec_len, data_len), - 257 => wire__crate__set_nwc_connection_info_impl(port, ptr, rust_vec_len, data_len), - 258 => wire__crate__set_nwc_daily_budget_sats_impl(port, ptr, rust_vec_len, data_len), - 259 => wire__crate__set_nwc_max_payment_sats_impl(port, ptr, rust_vec_len, data_len), - 260 => wire__crate__set_pin_code_impl(port, ptr, rust_vec_len, data_len), - 261 => wire__crate__set_require_pin_for_spending_impl(port, ptr, rust_vec_len, data_len), - 262 => wire__crate__set_show_msats_impl(port, ptr, rust_vec_len, data_len), - 263 => wire__crate__subscribe_deposits_impl(port, ptr, rust_vec_len, data_len), - 264 => wire__crate__subscribe_multimint_events_impl(port, ptr, rust_vec_len, data_len), - 265 => wire__crate__subscribe_peer_status_impl(port, ptr, rust_vec_len, data_len), - 266 => wire__crate__subscribe_recovery_progress_impl(port, ptr, rust_vec_len, data_len), - 267 => wire__crate__sync_contacts_impl(port, ptr, rust_vec_len, data_len), - 268 => wire__crate__transactions_impl(port, ptr, rust_vec_len, data_len), - 269 => wire__crate__verify_nip05_impl(port, ptr, rust_vec_len, data_len), - 270 => wire__crate__verify_pin_impl(port, ptr, rust_vec_len, data_len), - 271 => wire__crate__wallet_summary_impl(port, ptr, rust_vec_len, data_len), - 272 => wire__crate__withdraw_to_address_impl(port, ptr, rust_vec_len, data_len), - 273 => wire__crate__word_list_impl(port, ptr, rust_vec_len, data_len), + 247 => wire__crate__parsed_scanned_text_impl(port, ptr, rust_vec_len, data_len), + 248 => wire__crate__payment_preview_with_gateways_impl(port, ptr, rust_vec_len, data_len), + 249 => wire__crate__pin_lockout_seconds_impl(port, ptr, rust_vec_len, data_len), + 250 => wire__crate__receive_impl(port, ptr, rust_vec_len, data_len), + 251 => wire__crate__recheck_address_impl(port, ptr, rust_vec_len, data_len), + 252 => wire__crate__refresh_connections_impl(port, ptr, rust_vec_len, data_len), + 253 => wire__crate__refresh_federation_meta_impl(port, ptr, rust_vec_len, data_len), + 254 => wire__crate__register_ln_address_impl(port, ptr, rust_vec_len, data_len), + 255 => wire__crate__reissue_ecash_impl(port, ptr, rust_vec_len, data_len), + 256 => wire__crate__rejoin_from_backup_invites_impl(port, ptr, rust_vec_len, data_len), + 257 => wire__crate__remove_nwc_connection_info_impl(port, ptr, rust_vec_len, data_len), + 258 => wire__crate__remove_relay_impl(port, ptr, rust_vec_len, data_len), + 259 => wire__crate__send_impl(port, ptr, rust_vec_len, data_len), + 260 => wire__crate__send_ecash_impl(port, ptr, rust_vec_len, data_len), + 261 => wire__crate__set_bitcoin_display_impl(port, ptr, rust_vec_len, data_len), + 262 => wire__crate__set_federation_order_impl(port, ptr, rust_vec_len, data_len), + 263 => wire__crate__set_fiat_currency_impl(port, ptr, rust_vec_len, data_len), + 264 => wire__crate__set_nwc_connection_info_impl(port, ptr, rust_vec_len, data_len), + 265 => wire__crate__set_nwc_daily_budget_sats_impl(port, ptr, rust_vec_len, data_len), + 266 => wire__crate__set_nwc_max_payment_sats_impl(port, ptr, rust_vec_len, data_len), + 267 => wire__crate__set_pin_code_impl(port, ptr, rust_vec_len, data_len), + 268 => wire__crate__set_require_pin_for_spending_impl(port, ptr, rust_vec_len, data_len), + 269 => wire__crate__set_show_msats_impl(port, ptr, rust_vec_len, data_len), + 270 => wire__crate__set_tap_receive_enabled_impl(port, ptr, rust_vec_len, data_len), + 271 => wire__crate__subscribe_deposits_impl(port, ptr, rust_vec_len, data_len), + 272 => wire__crate__subscribe_multimint_events_impl(port, ptr, rust_vec_len, data_len), + 273 => wire__crate__subscribe_peer_status_impl(port, ptr, rust_vec_len, data_len), + 274 => wire__crate__subscribe_recovery_progress_impl(port, ptr, rust_vec_len, data_len), + 275 => wire__crate__sync_contacts_impl(port, ptr, rust_vec_len, data_len), + 276 => wire__crate__transactions_impl(port, ptr, rust_vec_len, data_len), + 277 => wire__crate__verify_nip05_impl(port, ptr, rust_vec_len, data_len), + 278 => wire__crate__verify_pin_impl(port, ptr, rust_vec_len, data_len), + 279 => wire__crate__wallet_summary_impl(port, ptr, rust_vec_len, data_len), + 280 => wire__crate__withdraw_to_address_impl(port, ptr, rust_vec_len, data_len), + 281 => wire__crate__word_list_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -18441,145 +18829,149 @@ fn pde_ffi_dispatcher_sync_impl( rust_vec_len, data_len, ), - 133 => { + 135 => { wire__crate__fountain__OobNotesDecoder_add_fragment_impl(ptr, rust_vec_len, data_len) } - 134 => wire__crate__fountain__OobNotesDecoder_new_impl(ptr, rust_vec_len, data_len), - 135 => wire__crate__fountain__OobNotesEncoder_new_impl(ptr, rust_vec_len, data_len), - 137 => { + 136 => wire__crate__fountain__OobNotesDecoder_new_impl(ptr, rust_vec_len, data_len), + 137 => wire__crate__fountain__OobNotesEncoder_new_impl(ptr, rust_vec_len, data_len), + 139 => { wire__crate__multimint__OobNotesWrapper_amount_msats_impl(ptr, rust_vec_len, data_len) } - 138 => wire__crate__multimint__OobNotesWrapper_to_string_impl(ptr, rust_vec_len, data_len), - 139 => wire__crate__nostr__PublicFederation_auto_accessor_get_about_impl( + 140 => wire__crate__multimint__OobNotesWrapper_to_string_impl(ptr, rust_vec_len, data_len), + 141 => wire__crate__nostr__PublicFederation_auto_accessor_get_about_impl( ptr, rust_vec_len, data_len, ), - 140 => wire__crate__nostr__PublicFederation_auto_accessor_get_federation_id_impl( + 142 => wire__crate__nostr__PublicFederation_auto_accessor_get_federation_id_impl( ptr, rust_vec_len, data_len, ), - 141 => wire__crate__nostr__PublicFederation_auto_accessor_get_federation_name_impl( + 143 => wire__crate__nostr__PublicFederation_auto_accessor_get_federation_name_impl( ptr, rust_vec_len, data_len, ), - 142 => wire__crate__nostr__PublicFederation_auto_accessor_get_invite_codes_impl( + 144 => wire__crate__nostr__PublicFederation_auto_accessor_get_invite_codes_impl( ptr, rust_vec_len, data_len, ), - 143 => wire__crate__nostr__PublicFederation_auto_accessor_get_modules_impl( + 145 => wire__crate__nostr__PublicFederation_auto_accessor_get_modules_impl( ptr, rust_vec_len, data_len, ), - 144 => wire__crate__nostr__PublicFederation_auto_accessor_get_network_impl( + 146 => wire__crate__nostr__PublicFederation_auto_accessor_get_network_impl( ptr, rust_vec_len, data_len, ), - 145 => wire__crate__nostr__PublicFederation_auto_accessor_get_picture_impl( + 147 => wire__crate__nostr__PublicFederation_auto_accessor_get_picture_impl( ptr, rust_vec_len, data_len, ), - 146 => wire__crate__nostr__PublicFederation_auto_accessor_set_about_impl( + 148 => wire__crate__nostr__PublicFederation_auto_accessor_set_about_impl( ptr, rust_vec_len, data_len, ), - 147 => wire__crate__nostr__PublicFederation_auto_accessor_set_federation_id_impl( + 149 => wire__crate__nostr__PublicFederation_auto_accessor_set_federation_id_impl( ptr, rust_vec_len, data_len, ), - 148 => wire__crate__nostr__PublicFederation_auto_accessor_set_federation_name_impl( + 150 => wire__crate__nostr__PublicFederation_auto_accessor_set_federation_name_impl( ptr, rust_vec_len, data_len, ), - 149 => wire__crate__nostr__PublicFederation_auto_accessor_set_invite_codes_impl( + 151 => wire__crate__nostr__PublicFederation_auto_accessor_set_invite_codes_impl( ptr, rust_vec_len, data_len, ), - 150 => wire__crate__nostr__PublicFederation_auto_accessor_set_modules_impl( + 152 => wire__crate__nostr__PublicFederation_auto_accessor_set_modules_impl( ptr, rust_vec_len, data_len, ), - 151 => wire__crate__nostr__PublicFederation_auto_accessor_set_network_impl( + 153 => wire__crate__nostr__PublicFederation_auto_accessor_set_network_impl( ptr, rust_vec_len, data_len, ), - 152 => wire__crate__nostr__PublicFederation_auto_accessor_set_picture_impl( + 154 => wire__crate__nostr__PublicFederation_auto_accessor_set_picture_impl( ptr, rust_vec_len, data_len, ), - 153 => { + 155 => wire__crate__tap_transfer__TapRecipient_decrypt_impl(ptr, rust_vec_len, data_len), + 156 => wire__crate__tap_transfer__TapRecipient_new_impl(ptr, rust_vec_len, data_len), + 157 => wire__crate__tap_transfer__TapRecipient_public_key_impl(ptr, rust_vec_len, data_len), + 158 => { wire__crate__multimint__WithdrawFeesResponse_auto_accessor_get_federation_fee_msats_impl( ptr, rust_vec_len, data_len, ) } - 154 => wire__crate__multimint__WithdrawFeesResponse_auto_accessor_get_fee_amount_impl( + 159 => wire__crate__multimint__WithdrawFeesResponse_auto_accessor_get_fee_amount_impl( ptr, rust_vec_len, data_len, ), - 155 => { + 160 => { wire__crate__multimint__WithdrawFeesResponse_auto_accessor_get_fee_rate_sats_per_vb_impl( ptr, rust_vec_len, data_len, ) } - 156 => wire__crate__multimint__WithdrawFeesResponse_auto_accessor_get_fees_impl( + 161 => wire__crate__multimint__WithdrawFeesResponse_auto_accessor_get_fees_impl( ptr, rust_vec_len, data_len, ), - 157 => wire__crate__multimint__WithdrawFeesResponse_auto_accessor_get_tx_size_vbytes_impl( + 162 => wire__crate__multimint__WithdrawFeesResponse_auto_accessor_get_tx_size_vbytes_impl( ptr, rust_vec_len, data_len, ), - 158 => { + 163 => { wire__crate__multimint__WithdrawFeesResponse_auto_accessor_set_federation_fee_msats_impl( ptr, rust_vec_len, data_len, ) } - 159 => wire__crate__multimint__WithdrawFeesResponse_auto_accessor_set_fee_amount_impl( + 164 => wire__crate__multimint__WithdrawFeesResponse_auto_accessor_set_fee_amount_impl( ptr, rust_vec_len, data_len, ), - 160 => { + 165 => { wire__crate__multimint__WithdrawFeesResponse_auto_accessor_set_fee_rate_sats_per_vb_impl( ptr, rust_vec_len, data_len, ) } - 161 => wire__crate__multimint__WithdrawFeesResponse_auto_accessor_set_fees_impl( + 166 => wire__crate__multimint__WithdrawFeesResponse_auto_accessor_set_fees_impl( ptr, rust_vec_len, data_len, ), - 162 => wire__crate__multimint__WithdrawFeesResponse_auto_accessor_set_tx_size_vbytes_impl( + 167 => wire__crate__multimint__WithdrawFeesResponse_auto_accessor_set_tx_size_vbytes_impl( ptr, rust_vec_len, data_len, ), - 227 => wire__crate__multimint__is_mintv2_ecash_impl(ptr, rust_vec_len, data_len), - 238 => wire__crate__multimint__parse_oob_notes_impl(ptr, rust_vec_len, data_len), + 190 => wire__crate__encrypt_ecash_for_tap_impl(ptr, rust_vec_len, data_len), + 234 => wire__crate__multimint__is_mintv2_ecash_impl(ptr, rust_vec_len, data_len), + 245 => wire__crate__multimint__parse_oob_notes_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -19060,6 +19452,21 @@ impl flutter_rust_bridge::IntoIntoDart> for SafeUrl { } } +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for TapRecipient { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + // Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for FrbWrapper { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { @@ -20660,6 +21067,13 @@ impl SseEncode for SafeUrl { } } +impl SseEncode for TapRecipient { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + impl SseEncode for WithdrawFees { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -21014,6 +21428,17 @@ impl SseEncode for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + impl SseEncode for RustOpaqueMoi> { @@ -22593,6 +23018,7 @@ mod io { use crate::fountain::*; use crate::multimint::*; use crate::nostr::*; + use crate::tap_transfer::*; use crate::*; use flutter_rust_bridge::for_generated::byteorder::{ NativeEndian, ReadBytesExt, WriteBytesExt, @@ -23034,6 +23460,20 @@ mod io { MoiArc::>::decrement_strong_count(ptr as _); } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_ecashapp_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_ecashapp_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_ecashapp_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( ptr: *const std::ffi::c_void, @@ -23079,6 +23519,7 @@ mod web { use crate::fountain::*; use crate::multimint::*; use crate::nostr::*; + use crate::tap_transfer::*; use crate::*; use flutter_rust_bridge::for_generated::byteorder::{ NativeEndian, ReadBytesExt, WriteBytesExt, @@ -23522,6 +23963,20 @@ mod web { MoiArc::>::decrement_strong_count(ptr as _); } + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTapRecipient( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + #[wasm_bindgen] pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWithdrawFees( ptr: *const std::ffi::c_void, diff --git a/rust/ecashapp/src/lib.rs b/rust/ecashapp/src/lib.rs index 79effc35..806484ba 100644 --- a/rust/ecashapp/src/lib.rs +++ b/rust/ecashapp/src/lib.rs @@ -13,6 +13,7 @@ mod net; mod nostr; mod parse; mod pin; +mod tap_transfer; mod wallet; mod words; use bitcoin::key::rand::rngs::OsRng; @@ -570,6 +571,18 @@ pub async fn send_ecash( .await } +/// Encrypt an ecash string for a tap-transfer recipient (Phase 1 of the NFC + +/// BLE "tap to send" feature). `recipient_pubkey` is the 33-byte compressed key +/// received over NFC; the returned blob is delivered to the receiver over BLE +/// and decrypted with `TapRecipient::decrypt`. See `tap_transfer.rs`. +#[frb(sync)] +pub fn encrypt_ecash_for_tap( + ecash: String, + recipient_pubkey: Vec, +) -> Result, EcashAppError> { + crate::tap_transfer::encrypt_ecash(&ecash, &recipient_pubkey) +} + async fn parse_ecash(federation_id: &FederationId, notes: &OOBNotes) -> anyhow::Result { let multimint = get_multimint(); multimint.parse_ecash(federation_id, notes).await @@ -1219,6 +1232,18 @@ pub async fn set_show_msats(show_msats: bool) { multimint.set_show_msats(show_msats).await; } +#[frb] +pub async fn get_tap_receive_enabled() -> bool { + let multimint = get_multimint(); + multimint.get_tap_receive_enabled().await +} + +#[frb] +pub async fn set_tap_receive_enabled(enabled: bool) { + let multimint = get_multimint(); + multimint.set_tap_receive_enabled(enabled).await; +} + #[frb] pub async fn has_pin_code() -> bool { get_pin_manager().has_pin_code().await diff --git a/rust/ecashapp/src/multimint.rs b/rust/ecashapp/src/multimint.rs index b5dd1b36..21a59e2f 100644 --- a/rust/ecashapp/src/multimint.rs +++ b/rust/ecashapp/src/multimint.rs @@ -6423,6 +6423,24 @@ impl Multimint { dbtx.commit_tx().await; } + pub async fn get_tap_receive_enabled(&self) -> bool { + let mut dbtx = self.db.begin_transaction_nc().await; + dbtx.get_value(&crate::db::TapReceiveEnabledKey) + .await + .is_some() + } + + pub async fn set_tap_receive_enabled(&self, enabled: bool) { + let mut dbtx = self.db.begin_transaction().await; + if enabled { + dbtx.insert_entry(&crate::db::TapReceiveEnabledKey, &()) + .await; + } else { + dbtx.remove_entry(&crate::db::TapReceiveEnabledKey).await; + } + dbtx.commit_tx().await; + } + pub async fn get_federation_order(&self) -> Option> { let mut dbtx = self.db.begin_transaction_nc().await; dbtx.get_value(&crate::db::FederationOrderKey) diff --git a/rust/ecashapp/src/tap_transfer.rs b/rust/ecashapp/src/tap_transfer.rs new file mode 100644 index 00000000..d9f00eab --- /dev/null +++ b/rust/ecashapp/src/tap_transfer.rs @@ -0,0 +1,275 @@ +//! Tap-to-send ecash: authenticated ECIES over secp256k1. +//! +//! This is the cryptographic core of the NFC + BLE "tap to send" feature. The +//! receiver generates an *ephemeral* keypair and hands its public key to the +//! sender over NFC (a ~4 cm, proximity-authenticated channel). The sender +//! encrypts the ecash string to that public key and ships the ciphertext over +//! BLE, which is treated as a fully untrusted transport. Only the holder of the +//! receiver's ephemeral private key can decrypt, so an eavesdropper on BLE sees +//! an opaque blob. +//! +//! Scheme (ECIES / ephemeral-static ECDH): +//! - Receiver ephemeral keypair `(r, R = r*G)`. `R` crosses NFC. +//! - Sender generates a fresh ephemeral keypair `(e, E = e*G)` per transfer. +//! - `shared = ECDH(e, R) = ECDH(r, E)` (secp256k1; the shared value is the +//! SHA-256 of the compressed shared point, as returned by `SharedSecret`). +//! - `key = HKDF-SHA256(ikm = shared, salt = R || E, info = HKDF_INFO)`. +//! - `ciphertext = ChaCha20-Poly1305(key, nonce).encrypt(ecash, aad = header)`. +//! +//! Both ephemeral keys make each transfer forward-secret. Binding `R` and `E` +//! into the KDF salt (and the header into the AEAD associated data) prevents an +//! attacker from swapping the sender's ephemeral key without failing decryption. +//! +//! Wire format of the blob handed to BLE: +//! `[ version (1) | E compressed (33) | nonce (12) | ciphertext+tag (N) ]` + +use bitcoin::key::rand::rngs::OsRng; +use bitcoin::key::rand::RngCore; +use bitcoin::secp256k1::{ecdh::SharedSecret, PublicKey, Secp256k1, SecretKey}; +use chacha20poly1305::aead::{Aead, KeyInit, Payload}; +use chacha20poly1305::{ChaCha20Poly1305, Nonce}; +use flutter_rust_bridge::frb; +use hkdf::Hkdf; +use sha2::Sha256; + +use crate::app_error::{EcashAppError, EcashAppResult}; + +use anyhow::{anyhow, bail}; + +/// Blob format version. Bump when the wire layout or crypto changes. +const TAP_VERSION: u8 = 1; +/// Length of a compressed secp256k1 public key. +const PUBKEY_LEN: usize = 33; +/// ChaCha20-Poly1305 nonce length (96 bits). +const NONCE_LEN: usize = 12; +/// Poly1305 authentication tag length. +const TAG_LEN: usize = 16; +/// `version || ephemeral pubkey` — also used verbatim as the AEAD associated data. +const HEADER_LEN: usize = 1 + PUBKEY_LEN; +/// Smallest possible valid blob: header + nonce + an empty ciphertext (tag only). +const MIN_BLOB_LEN: usize = HEADER_LEN + NONCE_LEN + TAG_LEN; +/// Domain-separation string for the HKDF expansion. +const HKDF_INFO: &[u8] = b"ecashapp-tap-transfer-v1"; + +/// Derive the 32-byte AEAD key from the ECDH shared secret, binding both +/// ephemeral public keys into the salt for domain separation. +fn derive_key( + shared: &[u8; 32], + recipient_pub: &[u8; PUBKEY_LEN], + ephemeral_pub: &[u8; PUBKEY_LEN], +) -> [u8; 32] { + let mut salt = Vec::with_capacity(2 * PUBKEY_LEN); + salt.extend_from_slice(recipient_pub); + salt.extend_from_slice(ephemeral_pub); + let hk = Hkdf::::new(Some(&salt), shared); + let mut key = [0u8; 32]; + hk.expand(HKDF_INFO, &mut key) + .expect("32 is a valid HKDF-SHA256 output length"); + key +} + +/// Encrypt `plaintext` to `recipient`'s public key, returning the wire blob. +pub(crate) fn encrypt(plaintext: &[u8], recipient: &PublicKey) -> anyhow::Result> { + let secp = Secp256k1::new(); + let mut rng = OsRng; + + let (ephemeral_sk, ephemeral_pk) = secp.generate_keypair(&mut rng); + let shared = SharedSecret::new(recipient, &ephemeral_sk); + let recipient_bytes = recipient.serialize(); + let ephemeral_bytes = ephemeral_pk.serialize(); + let key = derive_key(&shared.secret_bytes(), &recipient_bytes, &ephemeral_bytes); + + let mut nonce = [0u8; NONCE_LEN]; + rng.fill_bytes(&mut nonce); + + // header = version || ephemeral pubkey; reused as AEAD associated data so + // any tampering with it fails authentication. + let mut blob = Vec::with_capacity(HEADER_LEN + NONCE_LEN + plaintext.len() + TAG_LEN); + blob.push(TAP_VERSION); + blob.extend_from_slice(&ephemeral_bytes); + let aad = blob.clone(); + + blob.extend_from_slice(&nonce); + let cipher = ChaCha20Poly1305::new_from_slice(&key).expect("32-byte key is valid"); + let ciphertext = cipher + .encrypt( + Nonce::from_slice(&nonce), + Payload { + msg: plaintext, + aad: &aad, + }, + ) + .map_err(|_| anyhow!("tap transfer: encryption failed"))?; + blob.extend_from_slice(&ciphertext); + Ok(blob) +} + +/// Decrypt a wire blob using the recipient's ephemeral secret key. +pub(crate) fn decrypt(blob: &[u8], recipient_secret: &SecretKey) -> anyhow::Result> { + if blob.len() < MIN_BLOB_LEN { + bail!("tap transfer: blob too short"); + } + if blob[0] != TAP_VERSION { + bail!("tap transfer: unsupported blob version {}", blob[0]); + } + + let ephemeral_pk = PublicKey::from_slice(&blob[1..HEADER_LEN]) + .map_err(|_| anyhow!("tap transfer: invalid ephemeral public key"))?; + let nonce = &blob[HEADER_LEN..HEADER_LEN + NONCE_LEN]; + let ciphertext = &blob[HEADER_LEN + NONCE_LEN..]; + + let secp = Secp256k1::new(); + let shared = SharedSecret::new(&ephemeral_pk, recipient_secret); + let recipient_bytes = PublicKey::from_secret_key(&secp, recipient_secret).serialize(); + let ephemeral_bytes = ephemeral_pk.serialize(); + let key = derive_key(&shared.secret_bytes(), &recipient_bytes, &ephemeral_bytes); + + let cipher = ChaCha20Poly1305::new_from_slice(&key).expect("32-byte key is valid"); + let plaintext = cipher + .decrypt( + Nonce::from_slice(nonce), + Payload { + msg: ciphertext, + aad: &blob[0..HEADER_LEN], + }, + ) + .map_err(|_| anyhow!("tap transfer: decryption failed"))?; + Ok(plaintext) +} + +/// Encrypt an ecash string for a tap-transfer recipient's public key. +/// +/// `recipient_pubkey` is the 33-byte compressed key received over NFC. The +/// returned blob is delivered to the receiver over BLE. +pub(crate) fn encrypt_ecash(ecash: &str, recipient_pubkey: &[u8]) -> EcashAppResult> { + let recipient = PublicKey::from_slice(recipient_pubkey) + .map_err(|_| EcashAppError::other("tap transfer: invalid recipient public key"))?; + encrypt(ecash.as_bytes(), &recipient).map_err(EcashAppError::from) +} + +/// Receiver-side tap-transfer session. +/// +/// Holds an ephemeral keypair whose private half never crosses the bridge: +/// Dart only ever sees the opaque handle and the 33-byte [`public_key`]. Create +/// one per incoming transfer, hand its public key to the sender over NFC, then +/// feed the BLE blob to [`decrypt`](TapRecipient::decrypt). +#[frb(opaque)] +pub struct TapRecipient { + secret_key: SecretKey, + public_key: PublicKey, +} + +impl TapRecipient { + #[frb(sync)] + pub fn new() -> Self { + let secp = Secp256k1::new(); + let (secret_key, public_key) = secp.generate_keypair(&mut OsRng); + Self { + secret_key, + public_key, + } + } + + /// The 33-byte compressed public key to hand to the sender over NFC. + #[frb(sync)] + pub fn public_key(&self) -> Vec { + self.public_key.serialize().to_vec() + } + + /// Decrypt a blob produced by [`encrypt_ecash`], returning the original + /// ecash string ready to pass to `reissue_ecash`. + #[frb(sync)] + pub fn decrypt(&self, blob: Vec) -> Result { + let plaintext = decrypt(&blob, &self.secret_key).map_err(EcashAppError::from)?; + String::from_utf8(plaintext).map_err(|_| { + EcashAppError::other("tap transfer: decrypted payload was not valid UTF-8") + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn keypair() -> (SecretKey, PublicKey) { + Secp256k1::new().generate_keypair(&mut OsRng) + } + + #[test] + fn round_trip() { + let (sk, pk) = keypair(); + let msg = b"fed11qgqrsdtest-ecash-token-string"; + let blob = encrypt(msg, &pk).unwrap(); + assert_eq!(blob[0], TAP_VERSION); + assert!(blob.len() >= MIN_BLOB_LEN); + assert_eq!(decrypt(&blob, &sk).unwrap(), msg); + } + + #[test] + fn empty_plaintext_round_trip() { + let (sk, pk) = keypair(); + let blob = encrypt(b"", &pk).unwrap(); + assert_eq!(blob.len(), MIN_BLOB_LEN); + assert_eq!(decrypt(&blob, &sk).unwrap(), b""); + } + + #[test] + fn wrong_key_fails() { + let (_sk, pk) = keypair(); + let (other_sk, _) = keypair(); + let blob = encrypt(b"secret notes", &pk).unwrap(); + assert!(decrypt(&blob, &other_sk).is_err()); + } + + #[test] + fn tampered_ciphertext_detected() { + let (sk, pk) = keypair(); + let mut blob = encrypt(b"secret notes", &pk).unwrap(); + let last = blob.len() - 1; + blob[last] ^= 0x01; + assert!(decrypt(&blob, &sk).is_err()); + } + + #[test] + fn tampered_header_detected() { + let (sk, pk) = keypair(); + let mut blob = encrypt(b"secret notes", &pk).unwrap(); + blob[1] ^= 0x01; // corrupt the ephemeral pubkey (feeds the KDF and AAD) + assert!(decrypt(&blob, &sk).is_err()); + } + + #[test] + fn short_blob_rejected() { + let (sk, _) = keypair(); + assert!(decrypt(&[TAP_VERSION, 0, 0], &sk).is_err()); + } + + #[test] + fn bad_version_rejected() { + let (sk, pk) = keypair(); + let mut blob = encrypt(b"x", &pk).unwrap(); + blob[0] = 0xFF; + assert!(decrypt(&blob, &sk).is_err()); + } + + #[test] + fn distinct_ephemeral_keys_per_encrypt() { + // Two encryptions of the same message must differ (fresh ephemeral + nonce). + let (_sk, pk) = keypair(); + let a = encrypt(b"same", &pk).unwrap(); + let b = encrypt(b"same", &pk).unwrap(); + assert_ne!(a, b); + } + + #[test] + fn recipient_public_key_is_compressed() { + assert_eq!(TapRecipient::new().public_key().len(), PUBKEY_LEN); + } + + #[test] + fn recipient_decrypts_encrypt_ecash() { + let recipient = TapRecipient::new(); + let blob = encrypt_ecash("fed1testtoken", &recipient.public_key()).unwrap(); + assert_eq!(recipient.decrypt(blob).unwrap(), "fed1testtoken"); + } +}