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");
+ }
+}