first pass NFC pushtx
This commit is contained in:
parent
9fe6ab13ef
commit
24bfca106a
76
docs/nfc-pushtx.md
Normal file
76
docs/nfc-pushtx.md
Normal file
@ -0,0 +1,76 @@
|
||||
# NFC Push Tx
|
||||
|
||||
This feature allows single-tap broadcast of the freshly-signed transaction.
|
||||
|
||||
Once enabled with a URL, the COLDCARD will show the NFC animation
|
||||
after signing the transaction. When the user taps their phone, the
|
||||
phone will see an NFC tag with URL inside. That URL contains the
|
||||
signed transaction ready to go, and once opening in the mobile
|
||||
browser, that URL will load. The landing page will connect to a
|
||||
Bitcoin node (or similar) and send the transaction on the public
|
||||
Bitcoin network.
|
||||
|
||||
This feature is available on Q and Mk4 and requires NFC to be enabled.
|
||||
See `Advanced/Tools > NFC Push Tx`
|
||||
|
||||
## Protocol Spec
|
||||
|
||||
The COLDCARD needs a URL prefix. To that it appends some values:
|
||||
|
||||
- `t=...`
|
||||
- this is the transaction, in binary encoded with
|
||||
[base64url](https://datatracker.ietf.org/doc/html/rfc4648#section-5)
|
||||
|
||||
- `&c=...`
|
||||
- the rightmost 8 bytes of SHA256 over the transaction. Also `base64url` encoded.
|
||||
|
||||
- `&n=XTN`
|
||||
- if, and only if, the COLDCARD is set for Testnet, this value is appended to
|
||||
indicate that the transaction is for Testnet3 and not MainNet.
|
||||
- when RegTest is enabled, the value will be `XRT`
|
||||
|
||||
We provide a few default URL values to our customers, including one backend we
|
||||
will operate. The URL can also be directly entered by the customer. On the Q,
|
||||
it can be scanned from a QR code.
|
||||
|
||||
For COLDCARD backend, the url used is:
|
||||
|
||||
https://coldcard.com/pushtx#
|
||||
|
||||
The complete URL with a typical transaction might look like this (but longer):
|
||||
|
||||
https://coldcard.com/pushtx#t=AgAAAAMNCxXtp2GVYVhkRXHLMmdZFs4p3kbFK ⋯ ABf&c=uiSVRda-1tw
|
||||
|
||||
We are using hash symbol here so that our server logs do not get
|
||||
contaminated with the arguments. The landing page uses javascript
|
||||
to read the hash part of the URL and decodes from there. If you
|
||||
prefer, your URL can end with `?` and then the arguments will be
|
||||
sent by the phone's browser to your server. Your processing can be
|
||||
entirely done in the backend in this case.
|
||||
|
||||
## Expectations for the Backend
|
||||
|
||||
Your code should decode the transaction and check the SHA-256 hash
|
||||
matches. If it does not match, or if `c` value is missing, assume
|
||||
the URL has been truncated and report that to the user.
|
||||
|
||||
Once decoded, your code should immediately broadcast the transaction.
|
||||
A confirmation step is not required in our opinion. Once it is
|
||||
submitted to Bitcoin Core (or other API), any status response should
|
||||
be decoded and shown to the user so they know it is on it's way.
|
||||
If it was not accepted, please report the error to the user as
|
||||
clearly as possible.
|
||||
|
||||
Next, it would make sense to either link to the TXID on a block
|
||||
explorer to provide further proof that it has been sent and that
|
||||
it is now waiting in the mempool.
|
||||
|
||||
### Notes
|
||||
|
||||
- Complete URL might be as large as 8,000 bytes. Some web servers will not support beyond
|
||||
4k bytes and the NFC implementation of the phone may also have limits.
|
||||
- The service URL provided must end in `?` or `#` or `&`.
|
||||
- `base64url` values from COLDCARD will not have padding (`=` bytes) at end.
|
||||
- Honest backends will not log the IP address of incoming transactions, but there is
|
||||
no way to enforce that, and CloudFlare sees all.
|
||||
|
||||
@ -4,6 +4,8 @@ This lists the new changes that have not yet been published in a normal release.
|
||||
|
||||
# Shared Improvements - Both Mk4 and Q
|
||||
|
||||
- New Feature: PushTX: when enabled with a service provider's URL, you can tap the COLDCARD
|
||||
after signing with your phone and the transaction will be broadcast directly.
|
||||
- Enhancement: Stricter p2sh-p2wpkh validation
|
||||
- Enhancement: mention the need to remove old duress wallets before locking down temporary seed
|
||||
- Bugfix: Fix PSBTv2 `PSBT_GLOBAL_TX_MODIFIABLE` parsing
|
||||
|
||||
@ -2155,8 +2155,8 @@ async def _scan_any_qr(expect_secret=False, tmp=False):
|
||||
|
||||
PUSHTX_SUPPLIERS = [
|
||||
# (label, URL)
|
||||
('coldcard.com', 'https://coldcard.com/pushtx?' ),
|
||||
('mempool.space', 'https://mempool.space/tx/push?tx=' ),
|
||||
('coldcard.com', 'https://coldcard.com/pushtx#' ),
|
||||
('mempool.space', 'https://mempool.space/tx/push?' ),
|
||||
]
|
||||
|
||||
async def pushtx_setup_menu(*a):
|
||||
@ -2203,8 +2203,14 @@ async def pushtx_setup_menu(*a):
|
||||
while 1:
|
||||
nv = await ux_input_text(val, confirm_exit=True, scan_ok=True, prompt="Enter URL")
|
||||
# cleanup? URL validation?
|
||||
if nv and ('http://' not in nv and 'https://' not in nv) or len(nv) < 12:
|
||||
await ux_show_story("Must start with http:// or https://. Try again")
|
||||
if nv:
|
||||
if ('http://' not in nv and 'https://' not in nv):
|
||||
prob = "Must start with http:// or https://."
|
||||
elif len(nv) < 12:
|
||||
prob = "Too short."
|
||||
if nv[-1] not in '#?&':
|
||||
prob = "Final char must be # or ? or &."
|
||||
await ux_show_story(prob + " Try again.")
|
||||
val = nv
|
||||
continue
|
||||
break
|
||||
|
||||
@ -806,9 +806,20 @@ class ApproveTransaction(UserAuthorizedAction):
|
||||
except BaseException as exc:
|
||||
return await self.failure("PSBT output failed", exc)
|
||||
|
||||
from glob import NFC
|
||||
from glob import NFC, settings
|
||||
|
||||
if self.do_finalize and txid and not hsm_active:
|
||||
|
||||
# if NFC PushTx is enabled, do that w/o questions.
|
||||
url = settings.get('ptxurl', False)
|
||||
if NFC and url:
|
||||
try:
|
||||
await NFC.share_push_tx(url, txid, TXN_OUTPUT_OFFSET, self.result[0], self.result[1])
|
||||
return
|
||||
except:
|
||||
# continue normally if it fails, perhaps too big?
|
||||
pass
|
||||
|
||||
kq, kn = "(1)", "(3)"
|
||||
if version.has_qwerty:
|
||||
kq, kn = KEY_QR, KEY_NFC
|
||||
|
||||
@ -235,7 +235,7 @@ class Display:
|
||||
ch = (h+CELL_H) // CELL_H
|
||||
#print('pixel %dx%d @ (%d,%d) => %dx%d @ (%d,%d)' % (w, h, px,py, cw, ch, cx,cy))
|
||||
|
||||
for y in range(cy, cy+ch+1):
|
||||
for y in range(cy, cy+ch):
|
||||
for x in range(cx, cx+cw+1):
|
||||
try:
|
||||
self.last_buf[y][x] = self.next_buf[y][x] = 0xfffe
|
||||
|
||||
@ -239,6 +239,39 @@ class NFCHandler:
|
||||
|
||||
return await self.share_start(n)
|
||||
|
||||
async def share_push_tx(self, url, txid, file_offset, txn_len, txn_sha):
|
||||
# Given a signed TXN, we convert to URL which a web backend can broadcast directly
|
||||
# - using base64url encoding
|
||||
# - just appends to provided URL
|
||||
# - keeps showing it until they press CANCEL
|
||||
#
|
||||
if (txn_len * 1.4) >= MAX_NFC_SIZE:
|
||||
raise ValueError('too big')
|
||||
|
||||
from glob import PSRAM
|
||||
from utils import b2a_base64url
|
||||
from chains import current_chain
|
||||
|
||||
is_https = url.startswith('https://')
|
||||
if is_https:
|
||||
url = url[8:]
|
||||
|
||||
url += 't=' + b2a_base64url(PSRAM.read_at(file_offset, txn_len)) \
|
||||
+ '&c=' + b2a_base64url(txn_sha[-8:])
|
||||
|
||||
ch = current_chain()
|
||||
if ch.ctype != 'BTC':
|
||||
url += '&n=' + ch.ctype # XTN or XRT
|
||||
|
||||
n = ndef.ndefMaker()
|
||||
n.add_url(url, https=is_https)
|
||||
|
||||
while 1:
|
||||
done = await self.share_start(n, prompt="Tap to broadcast, CANCEL when done",
|
||||
line2="Signed TXID: %s⋯%s" % (txid[0:8], txid[-8:]))
|
||||
|
||||
if done: break
|
||||
|
||||
async def share_psbt(self, file_offset, psbt_len, psbt_sha, label=None):
|
||||
# we just signed something, share it over NFC
|
||||
if psbt_len >= MAX_NFC_SIZE:
|
||||
@ -297,7 +330,7 @@ class NFCHandler:
|
||||
self.write_dyn(GPO_CTRL_Dyn, 0x01) # GPO_EN
|
||||
self.read_dyn(IT_STS_Dyn) # clear interrupt
|
||||
|
||||
async def ux_animation(self, write_mode, allow_enter=True):
|
||||
async def ux_animation(self, write_mode, allow_enter=True, prompt=None, line2=None):
|
||||
# Run the pretty animation, and detect both when we are written, and/or key to exit/abort.
|
||||
# - similar when "read" and then removed from field
|
||||
# - return T if aborted by user
|
||||
@ -309,7 +342,9 @@ class NFCHandler:
|
||||
|
||||
if dis.has_lcd:
|
||||
dis.real_clear() # bugfix
|
||||
dis.text(None, -2, 'Tap phone to screen, or CANCEL.', dark=True)
|
||||
dis.text(None, -2, prompt or 'Tap phone to screen, or CANCEL.', dark=True)
|
||||
if line2:
|
||||
dis.text(None, -3, line2)
|
||||
else:
|
||||
from graphics_mk4 import Graphics
|
||||
frames = [getattr(Graphics, 'mk4_nfc_%d'%i) for i in range(1, 5)]
|
||||
|
||||
@ -180,7 +180,7 @@ class Base64Writer:
|
||||
|
||||
def b2a_base64url(s):
|
||||
# see <https://datatracker.ietf.org/doc/html/rfc4648#section-5>
|
||||
return b2a_base64(s).rstrip(b'=\n').replace(b'+', b'-').replace(b'/', b'_')
|
||||
return b2a_base64(s).rstrip(b'=\n').replace(b'+', b'-').replace(b'/', b'_').decode()
|
||||
|
||||
def swab32(n):
|
||||
# endian swap: 32 bits
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
//
|
||||
// AUTO-generated.
|
||||
//
|
||||
// built: 2024-05-08
|
||||
// version: 1.2.1Q
|
||||
// built: 2024-06-03
|
||||
// version: 1.2.2Q
|
||||
//
|
||||
#include <stdint.h>
|
||||
|
||||
// this overrides ports/stm32/fatfs_port.c
|
||||
uint32_t get_fattime(void) {
|
||||
return 0x58a80840UL;
|
||||
return 0x58c30840UL;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user