mirror of
https://github.com/Coldcard/push-tx.git
synced 2026-10-22 08:33:24 +00:00
add code
This commit is contained in:
19 files changed
+3549
No files matched your search
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
build/index.html
|
||||
@@ -0,0 +1,16 @@
|
||||
.PHONY: install
|
||||
install:
|
||||
npm install
|
||||
|
||||
.PHONY: build
|
||||
build: install
|
||||
npm run build && PUSHTX_SINGLE_FILE=1 npm run build
|
||||
|
||||
# hash and sign all important files - both the source and built versions
|
||||
.PHONY: sign
|
||||
sign:
|
||||
find . -type f \
|
||||
\( -iname '*.js' -o -iname '*.ts' -o -iname '*.html' -o -iname '*.css' -o -iname '*.json' \) \
|
||||
! -path './node_modules/*' \
|
||||
-exec shasum -a 256 {} \; > SHA256SUMS && \
|
||||
gpg --detach-sign --armor --digest-algo SHA256 --yes SHA256SUMS
|
||||
@@ -0,0 +1,46 @@
|
||||
# COLDCARD Website Implementation
|
||||
|
||||
This is the code for the NFC Push TX implementation at <https://coldcard.com/pushtx>.
|
||||
|
||||
## Overview
|
||||
|
||||
We use the APIs of mempool.space and blockstream.info to broadcast the transaction
|
||||
and for retrieving additional transaction details afterwards.
|
||||
|
||||
In an edge case where the transaction is already confirmed (e.g. same Push TX URL
|
||||
used twice), the APIs respond with a 400 error. If that happens, we use
|
||||
[bitcoinjs-lib](https://github.com/bitcoinjs/bitcoinjs-lib) to compute the TXID
|
||||
and try to fetch the details anyway.
|
||||
|
||||
## How to Build
|
||||
|
||||
To build, you need Node.js and NPM installed. There's a `Makefile` target for
|
||||
installing dependencies and running the build:
|
||||
|
||||
```
|
||||
make build
|
||||
```
|
||||
|
||||
This builds two versions:
|
||||
|
||||
- Separate JS and CSS files that power <https://coldcard.com/pushtx> - saved to `build/`
|
||||
- A self-contained HTML where all JS and CSS is inlined, which is useful for easy
|
||||
self-hosting - saved to `build-single-file/`.
|
||||
|
||||
## Don't Trust, Verify
|
||||
|
||||
The `SHA256SUMS` file includes hashes of all important files in this repo and is
|
||||
signed with the PGP key `3F4D 2119 6E14 FD39 3835 492D F960 23C5 8E14 72C9` in
|
||||
`SHA256SUMS.asc` (detached signature).
|
||||
|
||||
To check the signature:
|
||||
|
||||
```
|
||||
gpg --verify SHA256SUMS.asc
|
||||
```
|
||||
|
||||
To check the hashes match:
|
||||
|
||||
```
|
||||
shasum -a 256 -c SHA256SUMS
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
791562fe4df2457484ffeb21774990451c59ca1818ccf71003334fe2576dc9a0 ./index.html
|
||||
5ef88a316223b466c86c5e87991c982591b2b354d365a13d7451201c651e7ced ./build-single-file/index.html
|
||||
7f8469776f50d7d766cbe3e438e290baf2d91815a3d8233f248b6e412bc8c94e ./package-lock.json
|
||||
d035d1b528f544331f8c697da0d407427f409b4fbb6f86232ec1445c16e85a7e ./package.json
|
||||
68df121854e628c7d84ee41b7d86875626cc342aa0c8b0f939af322eb4ba1723 ./tsconfig.json
|
||||
648d982c9f40db8a813a7fbc9c971b63f85a84a07a307474f71f85653afbdfde ./build/index.html
|
||||
2462f20149c235e936de6f9420cc0a4630aeb9c26f5dc6c5b906c11edbfc720c ./build/pushtx.js
|
||||
31ebc58b766680c1000cabd7b7da7eeb943aff58ebc805beba608e15ffc7a3f6 ./build/pushtx.css
|
||||
2ede67555dd622a2be580b3e15fdde6463413f63e79913f54faa75d819a4cc01 ./vite.config.ts
|
||||
91d383919e958aa174cb525b7b1c415e50e125b32349673235ac850d88ea4a0b ./src/main.ts
|
||||
b41fd5552c271453ce7ee1c661e72495705e5da0e005828b17f4ad057ba91e87 ./src/types.ts
|
||||
65996936fbb042915f7b74a200fcdde7e410f32a669b1ab9597cfaa4b0faddb5 ./src/vite-env.d.ts
|
||||
4e6dd4e5d1689110a3645a7ee5f82fac8ec32d4aad8c00d906be60f64ebca678 ./src/style.css
|
||||
54cc1fff898ecd3f437d82947889bbcab5f861904e61c9c1eea9f792a3f32582 ./src/config.ts
|
||||
@@ -0,0 +1,16 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAABCAAdFiEEP00hGW4U/Tk4NUkt+WAjxY4UcskFAmZsZVgACgkQ+WAjxY4U
|
||||
csmR2w//QkLrh012rneiKhThO1xSgkeeFv05lKDmKDZHxB6m+0YATqmkgZx0dnwo
|
||||
X4rva4QBgqjr/H5sZI364pNi4B+pgzLV1PjEMvsvEcRMi8mQL9Y+GyTGUE0GndJW
|
||||
oWQ4aYcVsV0Qz0mcEGNsupe7NOFKZQhTooSULv3gOtfkwbAejlFJRBiddYQzc9aX
|
||||
GZYM9S3dVV5DK0zeNf8Z5UofEyFY6vJ8nP6bmVzGgzmI5gzjFKPNH8aLpyQvkdQI
|
||||
0VNZ1P4HZQH9iw4z7wIHCqmCdPdERx/3roQmQ++v83ieozCNelamkRdGWnT4sV9T
|
||||
PpigNofqolgxauf506fdfewPqQQ/Ir1EQGAE7G+E/lCxFe+P6lipPkwdJGr6v/g6
|
||||
Co1WhEij0r++COF4oj9TPQCDtsEhCP2vMbomDL+pnOIGPuNtZvKKLJiYa93+o5QR
|
||||
RWo5m1ZP9yU+xs8v+5Gp/xQAZQD7AYGllCkNG89L+bkXvuFbHBh20hWXjPisGMcb
|
||||
MyC5iN7L+x3lu/t2iq+XvmNo9K504WWMzmzVbI2FWxTabbEA0IwQSZhbZDeGHAYP
|
||||
tBfLnnOzlcYXS3RTJXPE5QrHop6UIyitrzVH/TkJABJOmWUKhm/to43lBA2+NNEg
|
||||
nOUrhJl0BnYHmPgTkjVqvuPzxe+2dgquYfbwJ/W+sPSsosSpySQ=
|
||||
=wiI5
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -0,0 +1,140 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>COLDCARD NFC Transaction Broadcast</title>
|
||||
<script type="module" crossorigin>var Ws=Object.defineProperty;var js=(e,t,r)=>t in e?Ws(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var Zt=(e,t,r)=>(js(e,typeof t!="symbol"?t+"":t,r),r);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const c of i)if(c.type==="childList")for(const s of c.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&n(s)}).observe(document,{childList:!0,subtree:!0});function r(i){const c={};return i.integrity&&(c.integrity=i.integrity),i.referrerPolicy&&(c.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?c.credentials="include":i.crossOrigin==="anonymous"?c.credentials="omit":c.credentials="same-origin",c}function n(i){if(i.ep)return;i.ep=!0;const c=r(i);fetch(i.href,c)}})();function Vs(e){if(e.__esModule)return e;var t=e.default;if(typeof t=="function"){var r=function n(){return this instanceof n?Reflect.construct(t,arguments,this.constructor):t.apply(this,arguments)};r.prototype=t.prototype}else r={};return Object.defineProperty(r,"__esModule",{value:!0}),Object.keys(e).forEach(function(n){var i=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(r,n,i.get?i:{enumerable:!0,get:function(){return e[n]}})}),r}var lo={},se={},Vr={};Vr.byteLength=qs;Vr.toByteArray=Ys;Vr.fromByteArray=Zs;var $e=[],Pe=[],Gs=typeof Uint8Array<"u"?Uint8Array:Array,wn="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";for(var Lt=0,Xs=wn.length;Lt<Xs;++Lt)$e[Lt]=wn[Lt],Pe[wn.charCodeAt(Lt)]=Lt;Pe[45]=62;Pe[95]=63;function yo(e){var t=e.length;if(t%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var r=e.indexOf("=");r===-1&&(r=t);var n=r===t?0:4-r%4;return[r,n]}function qs(e){var t=yo(e),r=t[0],n=t[1];return(r+n)*3/4-n}function zs(e,t,r){return(t+r)*3/4-r}function Ys(e){var t,r=yo(e),n=r[0],i=r[1],c=new Gs(zs(e,n,i)),s=0,a=i>0?n-4:n,p;for(p=0;p<a;p+=4)t=Pe[e.charCodeAt(p)]<<18|Pe[e.charCodeAt(p+1)]<<12|Pe[e.charCodeAt(p+2)]<<6|Pe[e.charCodeAt(p+3)],c[s++]=t>>16&255,c[s++]=t>>8&255,c[s++]=t&255;return i===2&&(t=Pe[e.charCodeAt(p)]<<2|Pe[e.charCodeAt(p+1)]>>4,c[s++]=t&255),i===1&&(t=Pe[e.charCodeAt(p)]<<10|Pe[e.charCodeAt(p+1)]<<4|Pe[e.charCodeAt(p+2)]>>2,c[s++]=t>>8&255,c[s++]=t&255),c}function Js(e){return $e[e>>18&63]+$e[e>>12&63]+$e[e>>6&63]+$e[e&63]}function Qs(e,t,r){for(var n,i=[],c=t;c<r;c+=3)n=(e[c]<<16&16711680)+(e[c+1]<<8&65280)+(e[c+2]&255),i.push(Js(n));return i.join("")}function Zs(e){for(var t,r=e.length,n=r%3,i=[],c=16383,s=0,a=r-n;s<a;s+=c)i.push(Qs(e,s,s+c>a?a:s+c));return n===1?(t=e[r-1],i.push($e[t>>2]+$e[t<<4&63]+"==")):n===2&&(t=(e[r-2]<<8)+e[r-1],i.push($e[t>>10]+$e[t>>4&63]+$e[t<<2&63]+"=")),i.join("")}var Vn={};/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */Vn.read=function(e,t,r,n,i){var c,s,a=i*8-n-1,p=(1<<a)-1,w=p>>1,f=-7,d=r?i-1:0,S=r?-1:1,y=e[t+d];for(d+=S,c=y&(1<<-f)-1,y>>=-f,f+=a;f>0;c=c*256+e[t+d],d+=S,f-=8);for(s=c&(1<<-f)-1,c>>=-f,f+=n;f>0;s=s*256+e[t+d],d+=S,f-=8);if(c===0)c=1-w;else{if(c===p)return s?NaN:(y?-1:1)*(1/0);s=s+Math.pow(2,n),c=c-w}return(y?-1:1)*s*Math.pow(2,c-n)};Vn.write=function(e,t,r,n,i,c){var s,a,p,w=c*8-i-1,f=(1<<w)-1,d=f>>1,S=i===23?Math.pow(2,-24)-Math.pow(2,-77):0,y=n?0:c-1,_=n?1:-1,T=t<0||t===0&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(a=isNaN(t)?1:0,s=f):(s=Math.floor(Math.log(t)/Math.LN2),t*(p=Math.pow(2,-s))<1&&(s--,p*=2),s+d>=1?t+=S/p:t+=S*Math.pow(2,1-d),t*p>=2&&(s++,p/=2),s+d>=f?(a=0,s=f):s+d>=1?(a=(t*p-1)*Math.pow(2,i),s=s+d):(a=t*Math.pow(2,d-1)*Math.pow(2,i),s=0));i>=8;e[r+y]=a&255,y+=_,a/=256,i-=8);for(s=s<<i|a,w+=i;w>0;e[r+y]=s&255,y+=_,s/=256,w-=8);e[r+y-_]|=T*128};/*!
|
||||
* The buffer module from node.js, for the browser.
|
||||
*
|
||||
* @author Feross Aboukhadijeh <https://feross.org>
|
||||
* @license MIT
|
||||
*/(function(e){const t=Vr,r=Vn,n=typeof Symbol=="function"&&typeof Symbol.for=="function"?Symbol.for("nodejs.util.inspect.custom"):null;e.Buffer=f,e.SlowBuffer=v,e.INSPECT_MAX_BYTES=50;const i=2147483647;e.kMaxLength=i;const{Uint8Array:c,ArrayBuffer:s,SharedArrayBuffer:a}=globalThis;f.TYPED_ARRAY_SUPPORT=p(),!f.TYPED_ARRAY_SUPPORT&&typeof console<"u"&&typeof console.error=="function"&&console.error("This browser lacks typed array (Uint8Array) support which is required by `buffer` v5.x. Use `buffer` v4.x if you require old browser support.");function p(){try{const h=new c(1),o={foo:function(){return 42}};return Object.setPrototypeOf(o,c.prototype),Object.setPrototypeOf(h,o),h.foo()===42}catch{return!1}}Object.defineProperty(f.prototype,"parent",{enumerable:!0,get:function(){if(f.isBuffer(this))return this.buffer}}),Object.defineProperty(f.prototype,"offset",{enumerable:!0,get:function(){if(f.isBuffer(this))return this.byteOffset}});function w(h){if(h>i)throw new RangeError('The value "'+h+'" is invalid for option "size"');const o=new c(h);return Object.setPrototypeOf(o,f.prototype),o}function f(h,o,u){if(typeof h=="number"){if(typeof o=="string")throw new TypeError('The "string" argument must be of type string. Received type number');return _(h)}return d(h,o,u)}f.poolSize=8192;function d(h,o,u){if(typeof h=="string")return T(h,o);if(s.isView(h))return x(h);if(h==null)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof h);if(Ue(h,s)||h&&Ue(h.buffer,s)||typeof a<"u"&&(Ue(h,a)||h&&Ue(h.buffer,a)))return N(h,o,u);if(typeof h=="number")throw new TypeError('The "value" argument must not be of type number. Received type number');const l=h.valueOf&&h.valueOf();if(l!=null&&l!==h)return f.from(l,o,u);const g=k(h);if(g)return g;if(typeof Symbol<"u"&&Symbol.toPrimitive!=null&&typeof h[Symbol.toPrimitive]=="function")return f.from(h[Symbol.toPrimitive]("string"),o,u);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof h)}f.from=function(h,o,u){return d(h,o,u)},Object.setPrototypeOf(f.prototype,c.prototype),Object.setPrototypeOf(f,c);function S(h){if(typeof h!="number")throw new TypeError('"size" argument must be of type number');if(h<0)throw new RangeError('The value "'+h+'" is invalid for option "size"')}function y(h,o,u){return S(h),h<=0?w(h):o!==void 0?typeof u=="string"?w(h).fill(o,u):w(h).fill(o):w(h)}f.alloc=function(h,o,u){return y(h,o,u)};function _(h){return S(h),w(h<0?0:m(h)|0)}f.allocUnsafe=function(h){return _(h)},f.allocUnsafeSlow=function(h){return _(h)};function T(h,o){if((typeof o!="string"||o==="")&&(o="utf8"),!f.isEncoding(o))throw new TypeError("Unknown encoding: "+o);const u=I(h,o)|0;let l=w(u);const g=l.write(h,o);return g!==u&&(l=l.slice(0,g)),l}function P(h){const o=h.length<0?0:m(h.length)|0,u=w(o);for(let l=0;l<o;l+=1)u[l]=h[l]&255;return u}function x(h){if(Ue(h,c)){const o=new c(h);return N(o.buffer,o.byteOffset,o.byteLength)}return P(h)}function N(h,o,u){if(o<0||h.byteLength<o)throw new RangeError('"offset" is outside of buffer bounds');if(h.byteLength<o+(u||0))throw new RangeError('"length" is outside of buffer bounds');let l;return o===void 0&&u===void 0?l=new c(h):u===void 0?l=new c(h,o):l=new c(h,o,u),Object.setPrototypeOf(l,f.prototype),l}function k(h){if(f.isBuffer(h)){const o=m(h.length)|0,u=w(o);return u.length===0||h.copy(u,0,0,o),u}if(h.length!==void 0)return typeof h.length!="number"||yn(h.length)?w(0):P(h);if(h.type==="Buffer"&&Array.isArray(h.data))return P(h.data)}function m(h){if(h>=i)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+i.toString(16)+" bytes");return h|0}function v(h){return+h!=h&&(h=0),f.alloc(+h)}f.isBuffer=function(o){return o!=null&&o._isBuffer===!0&&o!==f.prototype},f.compare=function(o,u){if(Ue(o,c)&&(o=f.from(o,o.offset,o.byteLength)),Ue(u,c)&&(u=f.from(u,u.offset,u.byteLength)),!f.isBuffer(o)||!f.isBuffer(u))throw new TypeError('The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array');if(o===u)return 0;let l=o.length,g=u.length;for(let b=0,A=Math.min(l,g);b<A;++b)if(o[b]!==u[b]){l=o[b],g=u[b];break}return l<g?-1:g<l?1:0},f.isEncoding=function(o){switch(String(o).toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"latin1":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return!0;default:return!1}},f.concat=function(o,u){if(!Array.isArray(o))throw new TypeError('"list" argument must be an Array of Buffers');if(o.length===0)return f.alloc(0);let l;if(u===void 0)for(u=0,l=0;l<o.length;++l)u+=o[l].length;const g=f.allocUnsafe(u);let b=0;for(l=0;l<o.length;++l){let A=o[l];if(Ue(A,c))b+A.length>g.length?(f.isBuffer(A)||(A=f.from(A)),A.copy(g,b)):c.prototype.set.call(g,A,b);else if(f.isBuffer(A))A.copy(g,b);else throw new TypeError('"list" argument must be an Array of Buffers');b+=A.length}return g};funcLine truncated
|
||||
`,bech32:"bc",bip32:{public:76067358,private:76066276},pubKeyHash:0,scriptHash:5,wif:128};ae.regtest={messagePrefix:`Bitcoin Signed Message:
|
||||
`,bech32:"bcrt",bip32:{public:70617039,private:70615956},pubKeyHash:111,scriptHash:196,wif:239};ae.testnet={messagePrefix:`Bitcoin Signed Message:
|
||||
`,bech32:"tb",bip32:{public:70617039,private:70615956},pubKeyHash:111,scriptHash:196,wif:239};var gn={},Gr={},En={},ze={};Object.defineProperty(ze,"__esModule",{value:!0});ze.encode=ze.decode=ze.check=void 0;function gc(e){if(e.length<8||e.length>72||e[0]!==48||e[1]!==e.length-2||e[2]!==2)return!1;const t=e[3];if(t===0||5+t>=e.length||e[4+t]!==2)return!1;const r=e[5+t];return!(r===0||6+t+r!==e.length||e[4]&128||t>1&&e[4]===0&&!(e[5]&128)||e[t+6]&128||r>1&&e[t+6]===0&&!(e[t+7]&128))}ze.check=gc;function Ec(e){if(e.length<8)throw new Error("DER sequence length is too short");if(e.length>72)throw new Error("DER sequence length is too long");if(e[0]!==48)throw new Error("Expected DER sequence");if(e[1]!==e.length-2)throw new Error("DER sequence length is invalid");if(e[2]!==2)throw new Error("Expected DER integer");const t=e[3];if(t===0)throw new Error("R length is zero");if(5+t>=e.length)throw new Error("R length is too long");if(e[4+t]!==2)throw new Error("Expected DER integer (2)");const r=e[5+t];if(r===0)throw new Error("S length is zero");if(6+t+r!==e.length)throw new Error("S length is invalid");if(e[4]&128)throw new Error("R value is negative");if(t>1&&e[4]===0&&!(e[5]&128))throw new Error("R value excessively padded");if(e[t+6]&128)throw new Error("S value is negative");if(r>1&&e[t+6]===0&&!(e[t+7]&128))throw new Error("S value excessively padded");return{r:e.slice(4,4+t),s:e.slice(6+t)}}ze.decode=Ec;function mc(e,t){const r=e.length,n=t.length;if(r===0)throw new Error("R length is zero");if(n===0)throw new Error("S length is zero");if(r>33)throw new Error("R length is too long");if(n>33)throw new Error("S length is too long");if(e[0]&128)throw new Error("R value is negative");if(t[0]&128)throw new Error("S value is negative");if(r>1&&e[0]===0&&!(e[1]&128))throw new Error("R value excessively padded");if(n>1&&t[0]===0&&!(t[1]&128))throw new Error("S value excessively padded");const i=E.allocUnsafe(6+r+n);return i[0]=48,i[1]=i.length-2,i[2]=2,i[3]=e.length,e.copy(i,4),i[4+r]=2,i[5+r]=t.length,t.copy(i,6+r),i}ze.encode=mc;var ft={};Object.defineProperty(ft,"__esModule",{value:!0});ft.REVERSE_OPS=ft.OPS=void 0;const Fn={OP_FALSE:0,OP_0:0,OP_PUSHDATA1:76,OP_PUSHDATA2:77,OP_PUSHDATA4:78,OP_1NEGATE:79,OP_RESERVED:80,OP_TRUE:81,OP_1:81,OP_2:82,OP_3:83,OP_4:84,OP_5:85,OP_6:86,OP_7:87,OP_8:88,OP_9:89,OP_10:90,OP_11:91,OP_12:92,OP_13:93,OP_14:94,OP_15:95,OP_16:96,OP_NOP:97,OP_VER:98,OP_IF:99,OP_NOTIF:100,OP_VERIF:101,OP_VERNOTIF:102,OP_ELSE:103,OP_ENDIF:104,OP_VERIFY:105,OP_RETURN:106,OP_TOALTSTACK:107,OP_FROMALTSTACK:108,OP_2DROP:109,OP_2DUP:110,OP_3DUP:111,OP_2OVER:112,OP_2ROT:113,OP_2SWAP:114,OP_IFDUP:115,OP_DEPTH:116,OP_DROP:117,OP_DUP:118,OP_NIP:119,OP_OVER:120,OP_PICK:121,OP_ROLL:122,OP_ROT:123,OP_SWAP:124,OP_TUCK:125,OP_CAT:126,OP_SUBSTR:127,OP_LEFT:128,OP_RIGHT:129,OP_SIZE:130,OP_INVERT:131,OP_AND:132,OP_OR:133,OP_XOR:134,OP_EQUAL:135,OP_EQUALVERIFY:136,OP_RESERVED1:137,OP_RESERVED2:138,OP_1ADD:139,OP_1SUB:140,OP_2MUL:141,OP_2DIV:142,OP_NEGATE:143,OP_ABS:144,OP_NOT:145,OP_0NOTEQUAL:146,OP_ADD:147,OP_SUB:148,OP_MUL:149,OP_DIV:150,OP_MOD:151,OP_LSHIFT:152,OP_RSHIFT:153,OP_BOOLAND:154,OP_BOOLOR:155,OP_NUMEQUAL:156,OP_NUMEQUALVERIFY:157,OP_NUMNOTEQUAL:158,OP_LESSTHAN:159,OP_GREATERTHAN:160,OP_LESSTHANOREQUAL:161,OP_GREATERTHANOREQUAL:162,OP_MIN:163,OP_MAX:164,OP_WITHIN:165,OP_RIPEMD160:166,OP_SHA1:167,OP_SHA256:168,OP_HASH160:169,OP_HASH256:170,OP_CODESEPARATOR:171,OP_CHECKSIG:172,OP_CHECKSIGVERIFY:173,OP_CHECKMULTISIG:174,OP_CHECKMULTISIGVERIFY:175,OP_NOP1:176,OP_NOP2:177,OP_CHECKLOCKTIMEVERIFY:177,OP_NOP3:178,OP_CHECKSEQUENCEVERIFY:178,OP_NOP4:179,OP_NOP5:180,OP_NOP6:181,OP_NOP7:182,OP_NOP8:183,OP_NOP9:184,OP_NOP10:185,OP_CHECKSIGADD:186,OP_PUBKEYHASH:253,OP_PUBKEY:254,OP_INVALIDOPCODE:255};ft.OPS=Fn;const go={};ft.REVERSE_OPS=go;for(const e of Object.keys(Fn)){const t=Fn[e];go[t]=e}var ut={};Object.defineProperty(ut,"__esModule",{value:!0});ut.decode=ut.encode=ut.encodingLength=void 0;const ct=ft;function Eo(e){return e<ct.OPS.OP_PUSHDATA1?1:e<=255?2:e<=65535?3:5}ut.encodingLength=Eo;function Sc(e,t,r){const n=Eo(t);return n===1?e.writeUInt8(t,r):n===2?(e.writeUInt8(ct.OPS.OP_PUSHDATA1,r),e.writeUInt8(t,r+1)):n===3?(e.writeUInt8(ct.OPS.OP_PUSHDATA2,r),e.writeUInt16LE(t,r+1)):(e.writeUInt8(ct.OPS.OP_PUSHDATA4,r),e.writeUInt32LE(t,r+1)),n}ut.encode=Sc;function bc(e,t){const r=e.readUInt8(t);let n,i;if(r<ct.OPS.OP_PUSHDATA1)n=r,i=1;else if(r===ct.OPS.OP_PUSHDATA1){if(t+2>e.length)return null;n=e.readUInt8(t+1),i=2}else if(r===ct.OPS.OP_PUSHDATA2){if(t+3>e.length)return null;n=e.readUInt16LE(t+1),i=3}else{if(t+5>e.length)return null;if(r!==ct.OPS.OP_PUSHDATA4)throw new Error("Unexpected opcode");n=e.readUInt32LE(t+1),i=5}return{opcode:r,number:n,size:i}}ut.decode=bc;var zt={};Object.defineProperty(zt,"__esModule",{value:!0});zt.encode=zt.decode=void 0;function _c(e,t,r){t=t||4,r=r===void 0?!0:r;const n=e.length;if(n===0)return 0;if(n>t)throw new TypeError("Script number overflow");if(r&&!(e[n-1]&127)&&(n<=1||Line truncated
|
||||
*********************
|
||||
PROCEED WITH CAUTION!
|
||||
*********************`),a=c.hashForSignature(e,w,s)}return{script:w,sighashType:s,hash:a}}function yh(e,t,r,n){const i=[];if(t.tapInternalKey){const s=_s(e,t,n);s&&i.push(s)}if(t.tapScriptSig){const s=t.tapScriptSig.map(a=>a.pubkey);i.push(...s)}return i.map(s=>Wn(e,t,r,s,n)).flat()}function _s(e,t,r){const{script:n}=wi(e,t,r);return(0,re.isP2TR)(n)?n.subarray(2,34):null}function fo(e){return e.length===64?e:e.subarray(0,64)}function Wn(e,t,r,n,i,c,s){const a=i.__TX,p=t.sighashType||we.Transaction.SIGHASH_DEFAULT;Ts(p,s);const w=r.map((_,T)=>wi(T,_,i)),f=w.map(_=>_.script),d=w.map(_=>_.value),S=[];if(t.tapInternalKey&&!c){const _=_s(e,t,i)||E.from([]);if((0,J.toXOnly)(n).equals(_)){const T=a.hashForWitnessV1(e,f,d,p);S.push({pubkey:n,hash:T})}}const y=(t.tapLeafScript||[]).filter(_=>(0,re.pubkeyInScript)(n,_.script)).map(_=>{const T=(0,oh.tapleafHash)({output:_.script,version:_.leafVersion});return Object.assign({hash:T},_)}).filter(_=>!c||c.equals(_.hash)).map(_=>{const T=a.hashForWitnessV1(e,f,d,p,_.hash);return{pubkey:n,hash:T,leafHash:_.hash}});return S.concat(y)}function Ts(e,t){if(t&&t.indexOf(e)<0){const r=mh(e);throw new Error(`Sighash type is not allowed. Retry the sign method passing the sighashTypes array of whitelisted types. Sighash type: ${r}`)}}function wh(e,t,r){let n;switch(t){case"multisig":const i=Eh(e,r);n=ve.p2ms({output:e,signatures:i});break;case"pubkey":n=ve.p2pk({output:e,signature:r[0].signature});break;case"pubkeyhash":n=ve.p2pkh({output:e,pubkey:r[0].pubkey,signature:r[0].signature});break;case"witnesspubkeyhash":n=ve.p2wpkh({output:e,pubkey:r[0].pubkey,signature:r[0].signature});break}return n}function gh(e,t,r){const n=r.__TX,i={script:null,isSegwit:!1,isP2SH:!1,isP2WSH:!1};if(i.isP2SH=!!t.redeemScript,i.isP2WSH=!!t.witnessScript,t.witnessScript)i.script=t.witnessScript;else if(t.redeemScript)i.script=t.redeemScript;else if(t.nonWitnessUtxo){const c=fn(r,t,e),s=n.ins[e].index;i.script=c.outs[s].script}else t.witnessUtxo&&(i.script=t.witnessUtxo.script);return(t.witnessScript||(0,re.isP2WPKH)(i.script))&&(i.isSegwit=!0),i}function ho(e,t,r){const n=(0,Se.checkForInput)(t,e);if(!n.bip32Derivation||n.bip32Derivation.length===0)throw new Error("Need bip32Derivation to sign with HD");const i=n.bip32Derivation.map(s=>{if(s.masterFingerprint.equals(r.fingerprint))return s}).filter(s=>!!s);if(i.length===0)throw new Error("Need one bip32Derivation masterFingerprint to match the HDSigner fingerprint");return i.map(s=>{const a=r.derivePath(s.path);if(!s.pubkey.equals(a.publicKey))throw new Error("pubkey did not match bip32Derivation");return a})}function Eh(e,t){return ve.p2ms({output:e}).pubkeys.map(n=>(t.filter(i=>i.pubkey.equals(n))[0]||{}).signature).filter(n=>!!n)}function Is(e){let t=0;function r(s){return t+=s,e.slice(t-s,t)}function n(){const s=ro.decode(e,t);return t+=ro.decode.bytes,s}function i(){return r(n())}function c(){const s=n(),a=[];for(let p=0;p<s;p++)a.push(i());return a}return c()}function mh(e){let t=e&we.Transaction.SIGHASH_ANYONECANPAY?"SIGHASH_ANYONECANPAY | ":"";switch(e&31){case we.Transaction.SIGHASH_ALL:t+="SIGHASH_ALL";break;case we.Transaction.SIGHASH_SINGLE:t+="SIGHASH_SINGLE";break;case we.Transaction.SIGHASH_NONE:t+="SIGHASH_NONE";break}return t}function jn(e,t,r){e.__NON_WITNESS_UTXO_BUF_CACHE[r]=t.nonWitnessUtxo;const n=we.Transaction.fromBuffer(t.nonWitnessUtxo);e.__NON_WITNESS_UTXO_TX_CACHE[r]=n;const i=e,c=r;delete t.nonWitnessUtxo,Object.defineProperty(t,"nonWitnessUtxo",{enumerable:!0,get(){const s=i.__NON_WITNESS_UTXO_BUF_CACHE[c],a=i.__NON_WITNESS_UTXO_TX_CACHE[c];if(s!==void 0)return s;{const p=a.toBuffer();return i.__NON_WITNESS_UTXO_BUF_CACHE[c]=p,p}},set(s){i.__NON_WITNESS_UTXO_BUF_CACHE[c]=s}})}function ks(e,t,r,n){let i=0;e.forEach((p,w)=>{if(n&&p.finalScriptSig&&(t.ins[w].script=p.finalScriptSig),n&&p.finalScriptWitness&&(t.ins[w].witness=Is(p.finalScriptWitness)),p.witnessUtxo)i+=p.witnessUtxo.value;else if(p.nonWitnessUtxo){const f=fn(r,p,w),d=t.ins[w].index,S=f.outs[d];i+=S.value}});const c=t.outs.reduce((p,w)=>p+w.value,0),s=i-c;if(s<0)throw new Error("Outputs are spending more than Inputs");const a=t.virtualSize();r.__FEE=s,r.__EXTRACTED_TX=t,r.__FEE_RATE=Math.floor(s/a)}function fn(e,t,r){const n=e.__NON_WITNESS_UTXO_TX_CACHE;return n[r]||jn(e,t,r),n[r]}function As(e,t,r){const{script:n}=wi(e,t,r);return n}function wi(e,t,r){if(t.witnessUtxo!==void 0)return{script:t.witnessUtxo.script,value:t.witnessUtxo.value};if(t.nonWitnessUtxo!==void 0){const i=fn(r,t,e).outs[r.__TX.ins[e].index];return{script:i.script,value:i.value}}else throw new Error("Can't find pubkey in input without Utxo data")}function Sh(e,t,r,n){const i=As(r,t,n),{meaningfulScript:c}=hn(i,r,"input",t.redeemScript,t.witnessScript);return(0,re.pubkeyInScript)(e,c)}function bh(e,t,r,n){const i=n.__TX.outs[r].script,{meaningfulScript:c}=hn(i,r,"output",t.redeemScript,t.witnessScript);return(0,re.pubkeyInScript)(e,c)}function _h(e){if(!e)return;const t=Ye.decompile(e);if(!tLine truncated
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||
<path d="m10.97 4.97-.02.022-3.473 4.425-2.093-2.094a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05"/>
|
||||
</svg>`,error:`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/>
|
||||
</svg>`,info:`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
|
||||
</svg>`,progress:`
|
||||
<svg class="pushtx-spin" width="32" height="32" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle style="opacity: 0.25;" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>`};async function dr(e){try{return[null,await e]}catch(t){return t instanceof Error?[t,null]:typeof t=="string"?[new Error(t),null]:[new Error,null]}}function po(e){const t=e.replace(/-/g,"+").replace(/_/g,"/").padEnd(e.length+(4-e.length%4)%4,"="),r=atob(t);return new Uint8Array([...r].map(n=>n.charCodeAt(0)))}class Bs extends Error{constructor(r,n,i,c){super();Zt(this,"url");Zt(this,"status");Zt(this,"statusText");Zt(this,"body");this.url=r,this.name="FetchStatusError",this.status=n,this.statusText=i,this.body=c}}const xs=async(e,t)=>{const r=await fetch(e,t);if(!r.ok){const[n,i]=await dr(r.text());throw new Bs(e.toString(),r.status,r.statusText,i)}return r};async function Ph(e,t){if(t!=="BTC"&&t!=="XTN")throw new Error("Unsupported network: "+t);const n=vs[t].map(s=>xs(s,{method:"POST",body:e.toHex()})),[i,c]=await dr(Promise.any(n).then(s=>s.text()));if(c)return c;if(i instanceof AggregateError){const s=i.errors.filter(a=>a instanceof Bs);if(s.length>0){let a=`
|
||||
<p>The transaction was rejected by all providers:</p>
|
||||
|
||||
<ul>
|
||||
`;throw s.forEach(p=>{a+=`<li>${p.url}: ${p.body}</li>`}),a+="</ul>",new Error(a)}}throw new Error("<p>Could not connect to any push servers. Make sure you are connected to the Internet and try again.</p>")}async function vh(e){e[0]==="#"&&(e=e.slice(1));const t=new URLSearchParams(e),r=t.get("t"),n=t.get("c"),i=t.get("n")||"BTC";if(!r)throw new Error("Invalid URL - missing transaction.");if(!n||n.length!==11)throw new Error("Invalid URL - missing or incomplete checksum. The URL is probably truncated");let c;if(i==="BTC"||i==="XTN")c=i;else throw i==="XRT"?new Error("Regtest transactions are not supported."):new Error(`Invalid URL. The network "${i}" is not recognized.`);let s,a;try{s=po(r),a=po(n)}catch{throw new Error("Invalid URL encoding. The URL is probably corrupted.")}const p=new Uint8Array(await crypto.subtle.digest("SHA-256",s));if(!a.every((f,d)=>f===p[d+24]))throw new Error("Checksum mismatch in URL. Some bytes corrupted in transit. Try again.");return{tx:lo.Transaction.fromBuffer(wo.from(s)),network:c}}async function Bh(e,t){if(t!=="BTC"&&t!=="XTN")throw new Error("Unsupported network: "+t);const n=vs[t].map(i=>xs(i+"/"+e).then(c=>c.json()));return await Promise.any(n)}function xh(e){const t=e.vin.map(n=>`
|
||||
<tr>
|
||||
<td>
|
||||
<span class="address">
|
||||
<span>${n.prevout.scriptpubkey_address.slice(0,-8)}</span>
|
||||
<span>${n.prevout.scriptpubkey_address.slice(-8)}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="value">${(n.prevout.value/1e8).toFixed(8)}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`).join(""),r=e.vout.map(n=>`
|
||||
<tr>
|
||||
<td>
|
||||
<span class="address">
|
||||
<span>${n.scriptpubkey_address.slice(0,-8)}</span>
|
||||
<span>${n.scriptpubkey_address.slice(-8)}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="value">${(n.value/1e8).toFixed(8)}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");return`
|
||||
<div class="pushtx-details">
|
||||
<div class="inputs">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Inputs</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${t}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="outputs">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Outputs</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
${r}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="fee">
|
||||
<strong>Fee:</strong> <span class="value">${(e.fee/1e8).toFixed(8)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`}function Kt(e,t){const r=Oh[e];return`
|
||||
<div class="pushtx-message pushtx-message--${e}" role="alert">
|
||||
${r}
|
||||
<div>${t}</div>
|
||||
</div>
|
||||
`}async function Us(){const e=document.querySelector(".pushtx-message-area"),t=document.querySelector(".pushtx-details-area");if(!e||!t)throw new Error("Need message and details areas in HTML.");if(e.innerHTML="",t.innerHTML="",!window.location.hash){const d=`
|
||||
<p><strong>Did you get here by accident?</strong></p>
|
||||
<p>
|
||||
This page is meant to be loaded together with transaction data using the
|
||||
<strong>COLDCARD NFC Push TX feature</strong>. The complete URL should look something like this (but longer):
|
||||
</p>
|
||||
|
||||
<p><code>https://coldcard.com/pushtx#t=AgAAAAMNCxXtp2GVYVhkRXHLMmdZFs4p3kbFK ⋯ ABf&c=uiSVRda-1tw</code></p>
|
||||
`;e.innerHTML=Kt("info",d);return}e.innerHTML=Kt("progress","Sending transaction, please wait...");const[r,n]=await dr(vh(window.location.hash));if(r){e.innerHTML=Kt("error",r.message);return}const{tx:i,network:c}=n,s=i.getId(),[a,p]=await dr(Ph(i,c)),[w,f]=await dr(Bh(s,c));if(p||f){const d=f!=null&&f.status.confirmed?"This transaction has already been confirmed.":"The transaction has been sent and is waiting to be confirmed.",S=Ah[c].map(([y,_])=>`<li><a href="${_}${s}" target="_blank" rel="noopener">${y}</a></li>`).join("");e.innerHTML=Kt("success",`<p>${d} Transaction ID:</p>
|
||||
|
||||
<p class="txid">${s}</p>
|
||||
|
||||
<p>Verify on a block explorer:</p>
|
||||
|
||||
<ul>${S}</ul>
|
||||
`),f&&(t.innerHTML=xh(f));return}if(a&&w)e.innerHTML=Kt("error",a.message);else if(w){e.innerHTML=Kt("error",w.message);return}}Us();window.addEventListener("hashchange",Us);
|
||||
</script>
|
||||
<style rel="stylesheet" crossorigin>.pushtx *{margin:0;padding:0;box-sizing:border-box;color:inherit!important}.pushtx ul{list-style-position:inside}.pushtx .monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.pushtx-spin{animation:spin 1s linear infinite}.pushtx-message{display:flex;gap:1rem;padding:1rem;border:1px solid transparent;border-radius:.25rem;width:100%}.pushtx-message>:first-child{flex-shrink:0}.pushtx-message>:last-child{overflow:hidden;overflow-wrap:break-word;display:flex;flex-direction:column;gap:.5rem}.pushtx-message--success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.pushtx-message--info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.pushtx-message--progress{color:#004085;background-color:#cce5ff;border-color:#b8daff}.pushtx-message--error{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.pushtx-message .txid{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:.875em;font-weight:700}.pushtx-details{display:flex;flex-direction:column;margin-block:2rem;padding:.5rem;border-radius:.25rem;overflow:hidden;gap:.5rem;border:1px solid #e2e8f0}.pushtx-details table{border-collapse:collapse;width:100%}.pushtx-details th{text-align:left}.pushtx-details :is(th,td){border-width:0px;padding-block:.125rem}.pushtx-details :is(td:first-child,th:first-child){width:75%;max-width:0px}.pushtx-details :is(td:last-child,th:last-child){text-align:right}.pushtx-details :is(.address,.value){font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.pushtx-details .address{display:flex;padding-right:1em}.pushtx-details .address :first-child{overflow:hidden;text-overflow:ellipsis}.pushtx-details .fee{text-align:right}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="pushtx" style="
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
max-width: 1024px;
|
||||
margin: 5vh auto;
|
||||
">
|
||||
|
||||
<h1 style="margin-bottom: 2rem;">COLDCARD NFC Transaction Broadcast</h1>
|
||||
|
||||
<div>
|
||||
<div class="pushtx-message-area"></div>
|
||||
<div class="pushtx-details-area"></div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
.pushtx *{margin:0;padding:0;box-sizing:border-box;color:inherit!important}.pushtx ul{list-style-position:inside}.pushtx .monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.pushtx-spin{animation:spin 1s linear infinite}.pushtx-message{display:flex;gap:1rem;padding:1rem;border:1px solid transparent;border-radius:.25rem;width:100%}.pushtx-message>:first-child{flex-shrink:0}.pushtx-message>:last-child{overflow:hidden;overflow-wrap:break-word;display:flex;flex-direction:column;gap:.5rem}.pushtx-message--success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.pushtx-message--info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.pushtx-message--progress{color:#004085;background-color:#cce5ff;border-color:#b8daff}.pushtx-message--error{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.pushtx-message .txid{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:.875em;font-weight:700}.pushtx-details{display:flex;flex-direction:column;margin-block:2rem;padding:.5rem;border-radius:.25rem;overflow:hidden;gap:.5rem;border:1px solid #e2e8f0}.pushtx-details table{border-collapse:collapse;width:100%}.pushtx-details th{text-align:left}.pushtx-details :is(th,td){border-width:0px;padding-block:.125rem}.pushtx-details :is(td:first-child,th:first-child){width:75%;max-width:0px}.pushtx-details :is(td:last-child,th:last-child){text-align:right}.pushtx-details :is(.address,.value){font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.pushtx-details .address{display:flex;padding-right:1em}.pushtx-details .address :first-child{overflow:hidden;text-overflow:ellipsis}.pushtx-details .fee{text-align:right}
|
||||
@@ -0,0 +1,111 @@
|
||||
var Ws=Object.defineProperty;var js=(e,t,r)=>t in e?Ws(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var Zt=(e,t,r)=>(js(e,typeof t!="symbol"?t+"":t,r),r);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const c of i)if(c.type==="childList")for(const s of c.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&n(s)}).observe(document,{childList:!0,subtree:!0});function r(i){const c={};return i.integrity&&(c.integrity=i.integrity),i.referrerPolicy&&(c.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?c.credentials="include":i.crossOrigin==="anonymous"?c.credentials="omit":c.credentials="same-origin",c}function n(i){if(i.ep)return;i.ep=!0;const c=r(i);fetch(i.href,c)}})();function Vs(e){if(e.__esModule)return e;var t=e.default;if(typeof t=="function"){var r=function n(){return this instanceof n?Reflect.construct(t,arguments,this.constructor):t.apply(this,arguments)};r.prototype=t.prototype}else r={};return Object.defineProperty(r,"__esModule",{value:!0}),Object.keys(e).forEach(function(n){var i=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(r,n,i.get?i:{enumerable:!0,get:function(){return e[n]}})}),r}var lo={},se={},Vr={};Vr.byteLength=qs;Vr.toByteArray=Ys;Vr.fromByteArray=Zs;var $e=[],Pe=[],Gs=typeof Uint8Array<"u"?Uint8Array:Array,wn="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";for(var Lt=0,Xs=wn.length;Lt<Xs;++Lt)$e[Lt]=wn[Lt],Pe[wn.charCodeAt(Lt)]=Lt;Pe[45]=62;Pe[95]=63;function yo(e){var t=e.length;if(t%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var r=e.indexOf("=");r===-1&&(r=t);var n=r===t?0:4-r%4;return[r,n]}function qs(e){var t=yo(e),r=t[0],n=t[1];return(r+n)*3/4-n}function zs(e,t,r){return(t+r)*3/4-r}function Ys(e){var t,r=yo(e),n=r[0],i=r[1],c=new Gs(zs(e,n,i)),s=0,a=i>0?n-4:n,p;for(p=0;p<a;p+=4)t=Pe[e.charCodeAt(p)]<<18|Pe[e.charCodeAt(p+1)]<<12|Pe[e.charCodeAt(p+2)]<<6|Pe[e.charCodeAt(p+3)],c[s++]=t>>16&255,c[s++]=t>>8&255,c[s++]=t&255;return i===2&&(t=Pe[e.charCodeAt(p)]<<2|Pe[e.charCodeAt(p+1)]>>4,c[s++]=t&255),i===1&&(t=Pe[e.charCodeAt(p)]<<10|Pe[e.charCodeAt(p+1)]<<4|Pe[e.charCodeAt(p+2)]>>2,c[s++]=t>>8&255,c[s++]=t&255),c}function Js(e){return $e[e>>18&63]+$e[e>>12&63]+$e[e>>6&63]+$e[e&63]}function Qs(e,t,r){for(var n,i=[],c=t;c<r;c+=3)n=(e[c]<<16&16711680)+(e[c+1]<<8&65280)+(e[c+2]&255),i.push(Js(n));return i.join("")}function Zs(e){for(var t,r=e.length,n=r%3,i=[],c=16383,s=0,a=r-n;s<a;s+=c)i.push(Qs(e,s,s+c>a?a:s+c));return n===1?(t=e[r-1],i.push($e[t>>2]+$e[t<<4&63]+"==")):n===2&&(t=(e[r-2]<<8)+e[r-1],i.push($e[t>>10]+$e[t>>4&63]+$e[t<<2&63]+"=")),i.join("")}var Vn={};/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */Vn.read=function(e,t,r,n,i){var c,s,a=i*8-n-1,p=(1<<a)-1,w=p>>1,f=-7,d=r?i-1:0,S=r?-1:1,y=e[t+d];for(d+=S,c=y&(1<<-f)-1,y>>=-f,f+=a;f>0;c=c*256+e[t+d],d+=S,f-=8);for(s=c&(1<<-f)-1,c>>=-f,f+=n;f>0;s=s*256+e[t+d],d+=S,f-=8);if(c===0)c=1-w;else{if(c===p)return s?NaN:(y?-1:1)*(1/0);s=s+Math.pow(2,n),c=c-w}return(y?-1:1)*s*Math.pow(2,c-n)};Vn.write=function(e,t,r,n,i,c){var s,a,p,w=c*8-i-1,f=(1<<w)-1,d=f>>1,S=i===23?Math.pow(2,-24)-Math.pow(2,-77):0,y=n?0:c-1,_=n?1:-1,T=t<0||t===0&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(a=isNaN(t)?1:0,s=f):(s=Math.floor(Math.log(t)/Math.LN2),t*(p=Math.pow(2,-s))<1&&(s--,p*=2),s+d>=1?t+=S/p:t+=S*Math.pow(2,1-d),t*p>=2&&(s++,p/=2),s+d>=f?(a=0,s=f):s+d>=1?(a=(t*p-1)*Math.pow(2,i),s=s+d):(a=t*Math.pow(2,d-1)*Math.pow(2,i),s=0));i>=8;e[r+y]=a&255,y+=_,a/=256,i-=8);for(s=s<<i|a,w+=i;w>0;e[r+y]=s&255,y+=_,s/=256,w-=8);e[r+y-_]|=T*128};/*!
|
||||
* The buffer module from node.js, for the browser.
|
||||
*
|
||||
* @author Feross Aboukhadijeh <https://feross.org>
|
||||
* @license MIT
|
||||
*/(function(e){const t=Vr,r=Vn,n=typeof Symbol=="function"&&typeof Symbol.for=="function"?Symbol.for("nodejs.util.inspect.custom"):null;e.Buffer=f,e.SlowBuffer=v,e.INSPECT_MAX_BYTES=50;const i=2147483647;e.kMaxLength=i;const{Uint8Array:c,ArrayBuffer:s,SharedArrayBuffer:a}=globalThis;f.TYPED_ARRAY_SUPPORT=p(),!f.TYPED_ARRAY_SUPPORT&&typeof console<"u"&&typeof console.error=="function"&&console.error("This browser lacks typed array (Uint8Array) support which is required by `buffer` v5.x. Use `buffer` v4.x if you require old browser support.");function p(){try{const h=new c(1),o={foo:function(){return 42}};return Object.setPrototypeOf(o,c.prototype),Object.setPrototypeOf(h,o),h.foo()===42}catch{return!1}}Object.defineProperty(f.prototype,"parent",{enumerable:!0,get:function(){if(f.isBuffer(this))return this.buffer}}),Object.defineProperty(f.prototype,"offset",{enumerable:!0,get:function(){if(f.isBuffer(this))return this.byteOffset}});function w(h){if(h>i)throw new RangeError('The value "'+h+'" is invalid for option "size"');const o=new c(h);return Object.setPrototypeOf(o,f.prototype),o}function f(h,o,u){if(typeof h=="number"){if(typeof o=="string")throw new TypeError('The "string" argument must be of type string. Received type number');return _(h)}return d(h,o,u)}f.poolSize=8192;function d(h,o,u){if(typeof h=="string")return T(h,o);if(s.isView(h))return x(h);if(h==null)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof h);if(Ue(h,s)||h&&Ue(h.buffer,s)||typeof a<"u"&&(Ue(h,a)||h&&Ue(h.buffer,a)))return N(h,o,u);if(typeof h=="number")throw new TypeError('The "value" argument must not be of type number. Received type number');const l=h.valueOf&&h.valueOf();if(l!=null&&l!==h)return f.from(l,o,u);const g=k(h);if(g)return g;if(typeof Symbol<"u"&&Symbol.toPrimitive!=null&&typeof h[Symbol.toPrimitive]=="function")return f.from(h[Symbol.toPrimitive]("string"),o,u);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof h)}f.from=function(h,o,u){return d(h,o,u)},Object.setPrototypeOf(f.prototype,c.prototype),Object.setPrototypeOf(f,c);function S(h){if(typeof h!="number")throw new TypeError('"size" argument must be of type number');if(h<0)throw new RangeError('The value "'+h+'" is invalid for option "size"')}function y(h,o,u){return S(h),h<=0?w(h):o!==void 0?typeof u=="string"?w(h).fill(o,u):w(h).fill(o):w(h)}f.alloc=function(h,o,u){return y(h,o,u)};function _(h){return S(h),w(h<0?0:m(h)|0)}f.allocUnsafe=function(h){return _(h)},f.allocUnsafeSlow=function(h){return _(h)};function T(h,o){if((typeof o!="string"||o==="")&&(o="utf8"),!f.isEncoding(o))throw new TypeError("Unknown encoding: "+o);const u=I(h,o)|0;let l=w(u);const g=l.write(h,o);return g!==u&&(l=l.slice(0,g)),l}function P(h){const o=h.length<0?0:m(h.length)|0,u=w(o);for(let l=0;l<o;l+=1)u[l]=h[l]&255;return u}function x(h){if(Ue(h,c)){const o=new c(h);return N(o.buffer,o.byteOffset,o.byteLength)}return P(h)}function N(h,o,u){if(o<0||h.byteLength<o)throw new RangeError('"offset" is outside of buffer bounds');if(h.byteLength<o+(u||0))throw new RangeError('"length" is outside of buffer bounds');let l;return o===void 0&&u===void 0?l=new c(h):u===void 0?l=new c(h,o):l=new c(h,o,u),Object.setPrototypeOf(l,f.prototype),l}function k(h){if(f.isBuffer(h)){const o=m(h.length)|0,u=w(o);return u.length===0||h.copy(u,0,0,o),u}if(h.length!==void 0)return typeof h.length!="number"||yn(h.length)?w(0):P(h);if(h.type==="Buffer"&&Array.isArray(h.data))return P(h.data)}function m(h){if(h>=i)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+i.toString(16)+" bytes");return h|0}function v(h){return+h!=h&&(h=0),f.alloc(+h)}f.isBuffer=function(o){return o!=null&&o._isBuffer===!0&&o!==f.prototype},f.compare=function(o,u){if(Ue(o,c)&&(o=f.from(o,o.offset,o.byteLength)),Ue(u,c)&&(u=f.from(u,u.offset,u.byteLength)),!f.isBuffer(o)||!f.isBuffer(u))throw new TypeError('The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array');if(o===u)return 0;let l=o.length,g=u.length;for(let b=0,A=Math.min(l,g);b<A;++b)if(o[b]!==u[b]){l=o[b],g=u[b];break}return l<g?-1:g<l?1:0},f.isEncoding=function(o){switch(String(o).toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"latin1":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return!0;default:return!1}},f.concat=function(o,u){if(!Array.isArray(o))throw new TypeError('"list" argument must be an Array of Buffers');if(o.length===0)return f.alloc(0);let l;if(u===void 0)for(u=0,l=0;l<o.length;++l)u+=o[l].length;const g=f.allocUnsafe(u);let b=0;for(l=0;l<o.length;++l){let A=o[l];if(Ue(A,c))b+A.length>g.length?(f.isBuffer(A)||(A=f.from(A)),A.copy(g,b)):c.prototype.set.call(g,A,b);else if(f.isBuffer(A))A.copy(g,b);else throw new TypeError('"list" argument must be an Array of Buffers');b+=A.length}return g};funcLine truncated
|
||||
`,bech32:"bc",bip32:{public:76067358,private:76066276},pubKeyHash:0,scriptHash:5,wif:128};ae.regtest={messagePrefix:`Bitcoin Signed Message:
|
||||
`,bech32:"bcrt",bip32:{public:70617039,private:70615956},pubKeyHash:111,scriptHash:196,wif:239};ae.testnet={messagePrefix:`Bitcoin Signed Message:
|
||||
`,bech32:"tb",bip32:{public:70617039,private:70615956},pubKeyHash:111,scriptHash:196,wif:239};var gn={},Gr={},En={},ze={};Object.defineProperty(ze,"__esModule",{value:!0});ze.encode=ze.decode=ze.check=void 0;function gc(e){if(e.length<8||e.length>72||e[0]!==48||e[1]!==e.length-2||e[2]!==2)return!1;const t=e[3];if(t===0||5+t>=e.length||e[4+t]!==2)return!1;const r=e[5+t];return!(r===0||6+t+r!==e.length||e[4]&128||t>1&&e[4]===0&&!(e[5]&128)||e[t+6]&128||r>1&&e[t+6]===0&&!(e[t+7]&128))}ze.check=gc;function Ec(e){if(e.length<8)throw new Error("DER sequence length is too short");if(e.length>72)throw new Error("DER sequence length is too long");if(e[0]!==48)throw new Error("Expected DER sequence");if(e[1]!==e.length-2)throw new Error("DER sequence length is invalid");if(e[2]!==2)throw new Error("Expected DER integer");const t=e[3];if(t===0)throw new Error("R length is zero");if(5+t>=e.length)throw new Error("R length is too long");if(e[4+t]!==2)throw new Error("Expected DER integer (2)");const r=e[5+t];if(r===0)throw new Error("S length is zero");if(6+t+r!==e.length)throw new Error("S length is invalid");if(e[4]&128)throw new Error("R value is negative");if(t>1&&e[4]===0&&!(e[5]&128))throw new Error("R value excessively padded");if(e[t+6]&128)throw new Error("S value is negative");if(r>1&&e[t+6]===0&&!(e[t+7]&128))throw new Error("S value excessively padded");return{r:e.slice(4,4+t),s:e.slice(6+t)}}ze.decode=Ec;function mc(e,t){const r=e.length,n=t.length;if(r===0)throw new Error("R length is zero");if(n===0)throw new Error("S length is zero");if(r>33)throw new Error("R length is too long");if(n>33)throw new Error("S length is too long");if(e[0]&128)throw new Error("R value is negative");if(t[0]&128)throw new Error("S value is negative");if(r>1&&e[0]===0&&!(e[1]&128))throw new Error("R value excessively padded");if(n>1&&t[0]===0&&!(t[1]&128))throw new Error("S value excessively padded");const i=E.allocUnsafe(6+r+n);return i[0]=48,i[1]=i.length-2,i[2]=2,i[3]=e.length,e.copy(i,4),i[4+r]=2,i[5+r]=t.length,t.copy(i,6+r),i}ze.encode=mc;var ft={};Object.defineProperty(ft,"__esModule",{value:!0});ft.REVERSE_OPS=ft.OPS=void 0;const Fn={OP_FALSE:0,OP_0:0,OP_PUSHDATA1:76,OP_PUSHDATA2:77,OP_PUSHDATA4:78,OP_1NEGATE:79,OP_RESERVED:80,OP_TRUE:81,OP_1:81,OP_2:82,OP_3:83,OP_4:84,OP_5:85,OP_6:86,OP_7:87,OP_8:88,OP_9:89,OP_10:90,OP_11:91,OP_12:92,OP_13:93,OP_14:94,OP_15:95,OP_16:96,OP_NOP:97,OP_VER:98,OP_IF:99,OP_NOTIF:100,OP_VERIF:101,OP_VERNOTIF:102,OP_ELSE:103,OP_ENDIF:104,OP_VERIFY:105,OP_RETURN:106,OP_TOALTSTACK:107,OP_FROMALTSTACK:108,OP_2DROP:109,OP_2DUP:110,OP_3DUP:111,OP_2OVER:112,OP_2ROT:113,OP_2SWAP:114,OP_IFDUP:115,OP_DEPTH:116,OP_DROP:117,OP_DUP:118,OP_NIP:119,OP_OVER:120,OP_PICK:121,OP_ROLL:122,OP_ROT:123,OP_SWAP:124,OP_TUCK:125,OP_CAT:126,OP_SUBSTR:127,OP_LEFT:128,OP_RIGHT:129,OP_SIZE:130,OP_INVERT:131,OP_AND:132,OP_OR:133,OP_XOR:134,OP_EQUAL:135,OP_EQUALVERIFY:136,OP_RESERVED1:137,OP_RESERVED2:138,OP_1ADD:139,OP_1SUB:140,OP_2MUL:141,OP_2DIV:142,OP_NEGATE:143,OP_ABS:144,OP_NOT:145,OP_0NOTEQUAL:146,OP_ADD:147,OP_SUB:148,OP_MUL:149,OP_DIV:150,OP_MOD:151,OP_LSHIFT:152,OP_RSHIFT:153,OP_BOOLAND:154,OP_BOOLOR:155,OP_NUMEQUAL:156,OP_NUMEQUALVERIFY:157,OP_NUMNOTEQUAL:158,OP_LESSTHAN:159,OP_GREATERTHAN:160,OP_LESSTHANOREQUAL:161,OP_GREATERTHANOREQUAL:162,OP_MIN:163,OP_MAX:164,OP_WITHIN:165,OP_RIPEMD160:166,OP_SHA1:167,OP_SHA256:168,OP_HASH160:169,OP_HASH256:170,OP_CODESEPARATOR:171,OP_CHECKSIG:172,OP_CHECKSIGVERIFY:173,OP_CHECKMULTISIG:174,OP_CHECKMULTISIGVERIFY:175,OP_NOP1:176,OP_NOP2:177,OP_CHECKLOCKTIMEVERIFY:177,OP_NOP3:178,OP_CHECKSEQUENCEVERIFY:178,OP_NOP4:179,OP_NOP5:180,OP_NOP6:181,OP_NOP7:182,OP_NOP8:183,OP_NOP9:184,OP_NOP10:185,OP_CHECKSIGADD:186,OP_PUBKEYHASH:253,OP_PUBKEY:254,OP_INVALIDOPCODE:255};ft.OPS=Fn;const go={};ft.REVERSE_OPS=go;for(const e of Object.keys(Fn)){const t=Fn[e];go[t]=e}var ut={};Object.defineProperty(ut,"__esModule",{value:!0});ut.decode=ut.encode=ut.encodingLength=void 0;const ct=ft;function Eo(e){return e<ct.OPS.OP_PUSHDATA1?1:e<=255?2:e<=65535?3:5}ut.encodingLength=Eo;function Sc(e,t,r){const n=Eo(t);return n===1?e.writeUInt8(t,r):n===2?(e.writeUInt8(ct.OPS.OP_PUSHDATA1,r),e.writeUInt8(t,r+1)):n===3?(e.writeUInt8(ct.OPS.OP_PUSHDATA2,r),e.writeUInt16LE(t,r+1)):(e.writeUInt8(ct.OPS.OP_PUSHDATA4,r),e.writeUInt32LE(t,r+1)),n}ut.encode=Sc;function bc(e,t){const r=e.readUInt8(t);let n,i;if(r<ct.OPS.OP_PUSHDATA1)n=r,i=1;else if(r===ct.OPS.OP_PUSHDATA1){if(t+2>e.length)return null;n=e.readUInt8(t+1),i=2}else if(r===ct.OPS.OP_PUSHDATA2){if(t+3>e.length)return null;n=e.readUInt16LE(t+1),i=3}else{if(t+5>e.length)return null;if(r!==ct.OPS.OP_PUSHDATA4)throw new Error("Unexpected opcode");n=e.readUInt32LE(t+1),i=5}return{opcode:r,number:n,size:i}}ut.decode=bc;var zt={};Object.defineProperty(zt,"__esModule",{value:!0});zt.encode=zt.decode=void 0;function _c(e,t,r){t=t||4,r=r===void 0?!0:r;const n=e.length;if(n===0)return 0;if(n>t)throw new TypeError("Script number overflow");if(r&&!(e[n-1]&127)&&(n<=1||
Line truncated
|
||||
*********************
|
||||
PROCEED WITH CAUTION!
|
||||
*********************`),a=c.hashForSignature(e,w,s)}return{script:w,sighashType:s,hash:a}}function yh(e,t,r,n){const i=[];if(t.tapInternalKey){const s=_s(e,t,n);s&&i.push(s)}if(t.tapScriptSig){const s=t.tapScriptSig.map(a=>a.pubkey);i.push(...s)}return i.map(s=>Wn(e,t,r,s,n)).flat()}function _s(e,t,r){const{script:n}=wi(e,t,r);return(0,re.isP2TR)(n)?n.subarray(2,34):null}function fo(e){return e.length===64?e:e.subarray(0,64)}function Wn(e,t,r,n,i,c,s){const a=i.__TX,p=t.sighashType||we.Transaction.SIGHASH_DEFAULT;Ts(p,s);const w=r.map((_,T)=>wi(T,_,i)),f=w.map(_=>_.script),d=w.map(_=>_.value),S=[];if(t.tapInternalKey&&!c){const _=_s(e,t,i)||E.from([]);if((0,J.toXOnly)(n).equals(_)){const T=a.hashForWitnessV1(e,f,d,p);S.push({pubkey:n,hash:T})}}const y=(t.tapLeafScript||[]).filter(_=>(0,re.pubkeyInScript)(n,_.script)).map(_=>{const T=(0,oh.tapleafHash)({output:_.script,version:_.leafVersion});return Object.assign({hash:T},_)}).filter(_=>!c||c.equals(_.hash)).map(_=>{const T=a.hashForWitnessV1(e,f,d,p,_.hash);return{pubkey:n,hash:T,leafHash:_.hash}});return S.concat(y)}function Ts(e,t){if(t&&t.indexOf(e)<0){const r=mh(e);throw new Error(`Sighash type is not allowed. Retry the sign method passing the sighashTypes array of whitelisted types. Sighash type: ${r}`)}}function wh(e,t,r){let n;switch(t){case"multisig":const i=Eh(e,r);n=ve.p2ms({output:e,signatures:i});break;case"pubkey":n=ve.p2pk({output:e,signature:r[0].signature});break;case"pubkeyhash":n=ve.p2pkh({output:e,pubkey:r[0].pubkey,signature:r[0].signature});break;case"witnesspubkeyhash":n=ve.p2wpkh({output:e,pubkey:r[0].pubkey,signature:r[0].signature});break}return n}function gh(e,t,r){const n=r.__TX,i={script:null,isSegwit:!1,isP2SH:!1,isP2WSH:!1};if(i.isP2SH=!!t.redeemScript,i.isP2WSH=!!t.witnessScript,t.witnessScript)i.script=t.witnessScript;else if(t.redeemScript)i.script=t.redeemScript;else if(t.nonWitnessUtxo){const c=fn(r,t,e),s=n.ins[e].index;i.script=c.outs[s].script}else t.witnessUtxo&&(i.script=t.witnessUtxo.script);return(t.witnessScript||(0,re.isP2WPKH)(i.script))&&(i.isSegwit=!0),i}function ho(e,t,r){const n=(0,Se.checkForInput)(t,e);if(!n.bip32Derivation||n.bip32Derivation.length===0)throw new Error("Need bip32Derivation to sign with HD");const i=n.bip32Derivation.map(s=>{if(s.masterFingerprint.equals(r.fingerprint))return s}).filter(s=>!!s);if(i.length===0)throw new Error("Need one bip32Derivation masterFingerprint to match the HDSigner fingerprint");return i.map(s=>{const a=r.derivePath(s.path);if(!s.pubkey.equals(a.publicKey))throw new Error("pubkey did not match bip32Derivation");return a})}function Eh(e,t){return ve.p2ms({output:e}).pubkeys.map(n=>(t.filter(i=>i.pubkey.equals(n))[0]||{}).signature).filter(n=>!!n)}function Is(e){let t=0;function r(s){return t+=s,e.slice(t-s,t)}function n(){const s=ro.decode(e,t);return t+=ro.decode.bytes,s}function i(){return r(n())}function c(){const s=n(),a=[];for(let p=0;p<s;p++)a.push(i());return a}return c()}function mh(e){let t=e&we.Transaction.SIGHASH_ANYONECANPAY?"SIGHASH_ANYONECANPAY | ":"";switch(e&31){case we.Transaction.SIGHASH_ALL:t+="SIGHASH_ALL";break;case we.Transaction.SIGHASH_SINGLE:t+="SIGHASH_SINGLE";break;case we.Transaction.SIGHASH_NONE:t+="SIGHASH_NONE";break}return t}function jn(e,t,r){e.__NON_WITNESS_UTXO_BUF_CACHE[r]=t.nonWitnessUtxo;const n=we.Transaction.fromBuffer(t.nonWitnessUtxo);e.__NON_WITNESS_UTXO_TX_CACHE[r]=n;const i=e,c=r;delete t.nonWitnessUtxo,Object.defineProperty(t,"nonWitnessUtxo",{enumerable:!0,get(){const s=i.__NON_WITNESS_UTXO_BUF_CACHE[c],a=i.__NON_WITNESS_UTXO_TX_CACHE[c];if(s!==void 0)return s;{const p=a.toBuffer();return i.__NON_WITNESS_UTXO_BUF_CACHE[c]=p,p}},set(s){i.__NON_WITNESS_UTXO_BUF_CACHE[c]=s}})}function ks(e,t,r,n){let i=0;e.forEach((p,w)=>{if(n&&p.finalScriptSig&&(t.ins[w].script=p.finalScriptSig),n&&p.finalScriptWitness&&(t.ins[w].witness=Is(p.finalScriptWitness)),p.witnessUtxo)i+=p.witnessUtxo.value;else if(p.nonWitnessUtxo){const f=fn(r,p,w),d=t.ins[w].index,S=f.outs[d];i+=S.value}});const c=t.outs.reduce((p,w)=>p+w.value,0),s=i-c;if(s<0)throw new Error("Outputs are spending more than Inputs");const a=t.virtualSize();r.__FEE=s,r.__EXTRACTED_TX=t,r.__FEE_RATE=Math.floor(s/a)}function fn(e,t,r){const n=e.__NON_WITNESS_UTXO_TX_CACHE;return n[r]||jn(e,t,r),n[r]}function As(e,t,r){const{script:n}=wi(e,t,r);return n}function wi(e,t,r){if(t.witnessUtxo!==void 0)return{script:t.witnessUtxo.script,value:t.witnessUtxo.value};if(t.nonWitnessUtxo!==void 0){const i=fn(r,t,e).outs[r.__TX.ins[e].index];return{script:i.script,value:i.value}}else throw new Error("Can't find pubkey in input without Utxo data")}function Sh(e,t,r,n){const i=As(r,t,n),{meaningfulScript:c}=hn(i,r,"input",t.redeemScript,t.witnessScript);return(0,re.pubkeyInScript)(e,c)}function bh(e,t,r,n){const i=n.__TX.outs[r].script,{meaningfulScript:c}=hn(i,r,"output",t.redeemScript,t.witnessScript);return(0,re.pubkeyInScript)(e,c)}function _h(e){if(!e)return;const t=Ye.decompile(e);if(!t
Line truncated
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||
<path d="m10.97 4.97-.02.022-3.473 4.425-2.093-2.094a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05"/>
|
||||
</svg>`,error:`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/>
|
||||
</svg>`,info:`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
|
||||
</svg>`,progress:`
|
||||
<svg class="pushtx-spin" width="32" height="32" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle style="opacity: 0.25;" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>`};async function dr(e){try{return[null,await e]}catch(t){return t instanceof Error?[t,null]:typeof t=="string"?[new Error(t),null]:[new Error,null]}}function po(e){const t=e.replace(/-/g,"+").replace(/_/g,"/").padEnd(e.length+(4-e.length%4)%4,"="),r=atob(t);return new Uint8Array([...r].map(n=>n.charCodeAt(0)))}class Bs extends Error{constructor(r,n,i,c){super();Zt(this,"url");Zt(this,"status");Zt(this,"statusText");Zt(this,"body");this.url=r,this.name="FetchStatusError",this.status=n,this.statusText=i,this.body=c}}const xs=async(e,t)=>{const r=await fetch(e,t);if(!r.ok){const[n,i]=await dr(r.text());throw new Bs(e.toString(),r.status,r.statusText,i)}return r};async function Ph(e,t){if(t!=="BTC"&&t!=="XTN")throw new Error("Unsupported network: "+t);const n=vs[t].map(s=>xs(s,{method:"POST",body:e.toHex()})),[i,c]=await dr(Promise.any(n).then(s=>s.text()));if(c)return c;if(i instanceof AggregateError){const s=i.errors.filter(a=>a instanceof Bs);if(s.length>0){let a=`
|
||||
<p>The transaction was rejected by all providers:</p>
|
||||
|
||||
<ul>
|
||||
`;throw s.forEach(p=>{a+=`<li>${p.url}: ${p.body}</li>`}),a+="</ul>",new Error(a)}}throw new Error("<p>Could not connect to any push servers. Make sure you are connected to the Internet and try again.</p>")}async function vh(e){e[0]==="#"&&(e=e.slice(1));const t=new URLSearchParams(e),r=t.get("t"),n=t.get("c"),i=t.get("n")||"BTC";if(!r)throw new Error("Invalid URL - missing transaction.");if(!n||n.length!==11)throw new Error("Invalid URL - missing or incomplete checksum. The URL is probably truncated");let c;if(i==="BTC"||i==="XTN")c=i;else throw i==="XRT"?new Error("Regtest transactions are not supported."):new Error(`Invalid URL. The network "${i}" is not recognized.`);let s,a;try{s=po(r),a=po(n)}catch{throw new Error("Invalid URL encoding. The URL is probably corrupted.")}const p=new Uint8Array(await crypto.subtle.digest("SHA-256",s));if(!a.every((f,d)=>f===p[d+24]))throw new Error("Checksum mismatch in URL. Some bytes corrupted in transit. Try again.");return{tx:lo.Transaction.fromBuffer(wo.from(s)),network:c}}async function Bh(e,t){if(t!=="BTC"&&t!=="XTN")throw new Error("Unsupported network: "+t);const n=vs[t].map(i=>xs(i+"/"+e).then(c=>c.json()));return await Promise.any(n)}function xh(e){const t=e.vin.map(n=>`
|
||||
<tr>
|
||||
<td>
|
||||
<span class="address">
|
||||
<span>${n.prevout.scriptpubkey_address.slice(0,-8)}</span>
|
||||
<span>${n.prevout.scriptpubkey_address.slice(-8)}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="value">${(n.prevout.value/1e8).toFixed(8)}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`).join(""),r=e.vout.map(n=>`
|
||||
<tr>
|
||||
<td>
|
||||
<span class="address">
|
||||
<span>${n.scriptpubkey_address.slice(0,-8)}</span>
|
||||
<span>${n.scriptpubkey_address.slice(-8)}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="value">${(n.value/1e8).toFixed(8)}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`).join("");return`
|
||||
<div class="pushtx-details">
|
||||
<div class="inputs">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Inputs</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${t}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="outputs">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Outputs</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
${r}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="fee">
|
||||
<strong>Fee:</strong> <span class="value">${(e.fee/1e8).toFixed(8)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`}function Kt(e,t){const r=Oh[e];return`
|
||||
<div class="pushtx-message pushtx-message--${e}" role="alert">
|
||||
${r}
|
||||
<div>${t}</div>
|
||||
</div>
|
||||
`}async function Us(){const e=document.querySelector(".pushtx-message-area"),t=document.querySelector(".pushtx-details-area");if(!e||!t)throw new Error("Need message and details areas in HTML.");if(e.innerHTML="",t.innerHTML="",!window.location.hash){const d=`
|
||||
<p><strong>Did you get here by accident?</strong></p>
|
||||
<p>
|
||||
This page is meant to be loaded together with transaction data using the
|
||||
<strong>COLDCARD NFC Push TX feature</strong>. The complete URL should look something like this (but longer):
|
||||
</p>
|
||||
|
||||
<p><code>https://coldcard.com/pushtx#t=AgAAAAMNCxXtp2GVYVhkRXHLMmdZFs4p3kbFK ⋯ ABf&c=uiSVRda-1tw</code></p>
|
||||
`;e.innerHTML=Kt("info",d);return}e.innerHTML=Kt("progress","Sending transaction, please wait...");const[r,n]=await dr(vh(window.location.hash));if(r){e.innerHTML=Kt("error",r.message);return}const{tx:i,network:c}=n,s=i.getId(),[a,p]=await dr(Ph(i,c)),[w,f]=await dr(Bh(s,c));if(p||f){const d=f!=null&&f.status.confirmed?"This transaction has already been confirmed.":"The transaction has been sent and is waiting to be confirmed.",S=Ah[c].map(([y,_])=>`<li><a href="${_}${s}" target="_blank" rel="noopener">${y}</a></li>`).join("");e.innerHTML=Kt("success",`<p>${d} Transaction ID:</p>
|
||||
|
||||
<p class="txid">${s}</p>
|
||||
|
||||
<p>Verify on a block explorer:</p>
|
||||
|
||||
<ul>${S}</ul>
|
||||
`),f&&(t.innerHTML=xh(f));return}if(a&&w)e.innerHTML=Kt("error",a.message);else if(w){e.innerHTML=Kt("error",w.message);return}}Us();window.addEventListener("hashchange",Us);
|
||||
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>COLDCARD NFC Transaction Broadcast</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="pushtx" style="
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
max-width: 1024px;
|
||||
margin: 5vh auto;
|
||||
">
|
||||
|
||||
<h1 style="margin-bottom: 2rem;">COLDCARD NFC Transaction Broadcast</h1>
|
||||
|
||||
<div>
|
||||
<div class="pushtx-message-area"></div>
|
||||
<div class="pushtx-details-area"></div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
# COLDCARD NFC Push TX
|
||||
|
||||
🔜
|
||||
Generated
+2465
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "coldcard-pushtx",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.14.2",
|
||||
"bitcoinjs-lib": "6.1.6",
|
||||
"typescript": "5.4.5",
|
||||
"vite": "5.2.13",
|
||||
"vite-plugin-node-polyfills": "0.22.0",
|
||||
"vite-plugin-singlefile": "2.0.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
export const TX_API_URLS = {
|
||||
// POST to these when pushing a TX (body is transaction HEX)
|
||||
// GET .../{txid} to get TX details
|
||||
BTC: ['https://mempool.space/api/tx', 'https://blockstream.info/api/tx'],
|
||||
XTN: ['https://mempool.space/testnet/api/tx', 'https://blockstream.info/testnet/api/tx'],
|
||||
} as const;
|
||||
|
||||
export const BLOCK_EXPLORER_CONFIG = {
|
||||
// Name and URL prefix of block explorers that we link to after TX is pushed
|
||||
// TXID will be appended to the URL
|
||||
BTC: [
|
||||
['mempool.space', 'https://mempool.space/tx/'],
|
||||
['blockstream.info', 'https://blockstream.info/tx/'],
|
||||
['btcscan.org', 'https://btcscan.org/tx/'],
|
||||
['btc.com', 'https://explorer.btc.com/btc/transaction/'],
|
||||
],
|
||||
XTN: [
|
||||
['mempool.space', 'https://mempool.space/testnet/tx/'],
|
||||
['blockstream.info', 'https://blockstream.info/testnet/tx/'],
|
||||
['blockcypher.com', 'https://live.blockcypher.com/btc-testnet/tx/'],
|
||||
],
|
||||
} as const;
|
||||
|
||||
export const MESSAGE_ICONS = {
|
||||
// SVGs for icons/spinner on the message box
|
||||
// - taken from https://icons.getbootstrap.com
|
||||
// - but inlining because easier to bundle into a single file this way
|
||||
|
||||
success: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||
<path d="m10.97 4.97-.02.022-3.473 4.425-2.093-2.094a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05"/>
|
||||
</svg>`,
|
||||
|
||||
error: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/>
|
||||
</svg>`,
|
||||
|
||||
info: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
|
||||
</svg>`,
|
||||
|
||||
// note: spinning animation is defined in CSS file
|
||||
progress: `
|
||||
<svg class="pushtx-spin" width="32" height="32" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle style="opacity: 0.25;" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>`,
|
||||
} as const;
|
||||
@@ -0,0 +1,398 @@
|
||||
import { Transaction } from 'bitcoinjs-lib';
|
||||
import { Buffer } from 'buffer';
|
||||
import { BLOCK_EXPLORER_CONFIG, MESSAGE_ICONS, TX_API_URLS } from './config';
|
||||
import './style.css';
|
||||
import { MempoolTxData, Network } from './types';
|
||||
|
||||
/**
|
||||
* Helper for awaiting either the return value or error of a promise.
|
||||
* Very useful for avoiding endless try/catch nesting and scoping hell.
|
||||
*/
|
||||
async function collect<T>(promise: Promise<T>): Promise<[null, T] | [Error, null]> {
|
||||
try {
|
||||
return [null, await promise];
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return [err, null];
|
||||
} else if (typeof err === 'string') {
|
||||
return [new Error(err), null];
|
||||
}
|
||||
|
||||
return [new Error(), null];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a base64url string to a Uint8Array
|
||||
*/
|
||||
function b64UrlToBytes(base64Url: string): Uint8Array {
|
||||
const base64 = base64Url
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/')
|
||||
.padEnd(base64Url.length + ((4 - (base64Url.length % 4)) % 4), '=');
|
||||
const binaryString = atob(base64);
|
||||
return new Uint8Array([...binaryString].map((char) => char.charCodeAt(0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error for fetchSafe below.
|
||||
*/
|
||||
class FetchStatusError extends Error {
|
||||
url: string;
|
||||
status: number;
|
||||
statusText: string;
|
||||
body: string | null;
|
||||
|
||||
constructor(url: string, status: number, statusText: string, body: string | null) {
|
||||
super();
|
||||
this.url = url;
|
||||
this.name = 'FetchStatusError';
|
||||
this.status = status;
|
||||
this.statusText = statusText;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around fetch that throws an error if the response is not 2xx.
|
||||
*/
|
||||
const fetchSafe: typeof fetch = async (input, init) => {
|
||||
const resp = await fetch(input, init);
|
||||
|
||||
if (!resp.ok) {
|
||||
const [_, body] = await collect(resp.text());
|
||||
throw new FetchStatusError(input.toString(), resp.status, resp.statusText, body);
|
||||
}
|
||||
|
||||
return resp;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a transaction to some providers:
|
||||
* - Currently mempool.space and blockstream.info, perhaps more in the future
|
||||
* - Supporting mainnet and testnet
|
||||
*/
|
||||
async function pushTx(tx: Transaction, network: Network): Promise<string> {
|
||||
if (network !== 'BTC' && network !== 'XTN') {
|
||||
throw new Error('Unsupported network: ' + network);
|
||||
}
|
||||
|
||||
const urls = TX_API_URLS[network];
|
||||
|
||||
const promises = urls.map((url) => fetchSafe(url, { method: 'POST', body: tx.toHex() }));
|
||||
|
||||
const [err, txid] = await collect(Promise.any(promises).then((res) => res.text()));
|
||||
|
||||
if (txid) {
|
||||
return txid;
|
||||
}
|
||||
|
||||
if (err instanceof AggregateError) {
|
||||
const fetchStatusErrors = err.errors.filter(
|
||||
(e): e is FetchStatusError => e instanceof FetchStatusError
|
||||
);
|
||||
|
||||
if (fetchStatusErrors.length > 0) {
|
||||
let errMsg = `
|
||||
<p>The transaction was rejected by all providers:</p>
|
||||
|
||||
<ul>
|
||||
`;
|
||||
|
||||
fetchStatusErrors.forEach((e) => {
|
||||
errMsg += `<li>${e.url}: ${e.body}</li>`;
|
||||
});
|
||||
|
||||
errMsg += `</ul>`;
|
||||
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`<p>Could not connect to any push servers. Make sure you are connected to the Internet and try again.</p>`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the transaction data and network from the URL fragment
|
||||
* @param fragment - The URL fragment, e.g. #t=base64tx&c=base64checksum&n=XTN
|
||||
*/
|
||||
async function parseFragment(fragment: string) {
|
||||
if (fragment[0] === '#') {
|
||||
fragment = fragment.slice(1);
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(fragment);
|
||||
|
||||
const t = params.get('t');
|
||||
const c = params.get('c');
|
||||
const n = params.get('n') || 'BTC';
|
||||
|
||||
if (!t) {
|
||||
throw new Error('Invalid URL - missing transaction.');
|
||||
}
|
||||
|
||||
if (!c || c.length !== 11) {
|
||||
throw new Error('Invalid URL - missing or incomplete checksum. The URL is probably truncated');
|
||||
}
|
||||
|
||||
let network: Network;
|
||||
|
||||
if (n === 'BTC' || n === 'XTN') {
|
||||
network = n;
|
||||
} else if (n === 'XRT') {
|
||||
throw new Error('Regtest transactions are not supported.');
|
||||
} else {
|
||||
throw new Error(`Invalid URL. The network "${n}" is not recognized.`);
|
||||
}
|
||||
|
||||
let txBytes: Uint8Array;
|
||||
let checkBytes: Uint8Array;
|
||||
|
||||
try {
|
||||
txBytes = b64UrlToBytes(t);
|
||||
checkBytes = b64UrlToBytes(c);
|
||||
} catch (err) {
|
||||
throw new Error('Invalid URL encoding. The URL is probably corrupted.');
|
||||
}
|
||||
|
||||
const txHash = new Uint8Array(await crypto.subtle.digest('SHA-256', txBytes));
|
||||
|
||||
if (!checkBytes.every((byte, i) => byte === txHash[i + 24])) {
|
||||
throw new Error('Checksum mismatch in URL. Some bytes corrupted in transit. Try again.');
|
||||
}
|
||||
|
||||
const tx = Transaction.fromBuffer(Buffer.from(txBytes));
|
||||
|
||||
return {
|
||||
tx,
|
||||
network,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the transaction details from mempool/blockstream to render useful information
|
||||
* that we can't (easily) get from the transaction itself, e.g. the input and output addresses
|
||||
* - Cancel other requests once the first one comes back OK.
|
||||
*/
|
||||
async function fetchTxData(txid: string, network: Network): Promise<MempoolTxData> {
|
||||
if (network !== 'BTC' && network !== 'XTN') {
|
||||
throw new Error('Unsupported network: ' + network);
|
||||
}
|
||||
|
||||
const urls = TX_API_URLS[network];
|
||||
|
||||
const promises = urls.map((url) => fetchSafe(url + '/' + txid).then((resp) => resp.json()));
|
||||
|
||||
return await Promise.any(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML for the transaction details that can be rendered on the page.
|
||||
*/
|
||||
function renderTxDetails(txData: MempoolTxData): string {
|
||||
const inputRows = txData.vin
|
||||
.map((input) => {
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<span class="address">
|
||||
<span>${input.prevout.scriptpubkey_address.slice(0, -8)}</span>
|
||||
<span>${input.prevout.scriptpubkey_address.slice(-8)}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="value">${(input.prevout.value / 1e8).toFixed(8)}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
const outputRows = txData.vout
|
||||
.map((output) => {
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<span class="address">
|
||||
<span>${output.scriptpubkey_address.slice(0, -8)}</span>
|
||||
<span>${output.scriptpubkey_address.slice(-8)}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="value">${(output.value / 1e8).toFixed(8)}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<div class="pushtx-details">
|
||||
<div class="inputs">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Inputs</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${inputRows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="outputs">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Outputs</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
${outputRows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="fee">
|
||||
<strong>Fee:</strong> <span class="value">${(txData.fee / 1e8).toFixed(8)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML for a message box that can be rendered on the page.
|
||||
* @param type - The type of message, e.g. 'success', 'error', 'info'
|
||||
* @param message - The message to display - can be HTML
|
||||
*/
|
||||
function renderMessage(type: 'success' | 'error' | 'info' | 'progress', message: string): string {
|
||||
const icon = MESSAGE_ICONS[type];
|
||||
|
||||
return `
|
||||
<div class="pushtx-message pushtx-message--${type}" role="alert">
|
||||
${icon}
|
||||
<div>${message}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* For testing - convert an exisiting HEX transaction to a URL fragment
|
||||
*/
|
||||
async function txToUrlFragment(hex: string, network: Network) {
|
||||
const txBytes = new Uint8Array(hex.match(/.{2}/g)!.map((byte) => parseInt(byte, 16)));
|
||||
const txHash = new Uint8Array(await crypto.subtle.digest('SHA-256', txBytes));
|
||||
|
||||
const tx = btoa(String.fromCharCode(...txBytes))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
const check = btoa(String.fromCharCode(...txHash.slice(24)))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
|
||||
let fragment = `#t=${tx}&c=${check}`;
|
||||
|
||||
if (network) {
|
||||
fragment += `&n=${network}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const messageArea = document.querySelector<HTMLDivElement>('.pushtx-message-area');
|
||||
const detailsArea = document.querySelector<HTMLDivElement>('.pushtx-details-area');
|
||||
|
||||
if (!messageArea || !detailsArea) {
|
||||
throw new Error('Need message and details areas in HTML.');
|
||||
}
|
||||
|
||||
messageArea.innerHTML = '';
|
||||
detailsArea.innerHTML = '';
|
||||
|
||||
if (!window.location.hash) {
|
||||
const msg = `
|
||||
<p><strong>Did you get here by accident?</strong></p>
|
||||
<p>
|
||||
This page is meant to be loaded together with transaction data using the
|
||||
<strong>COLDCARD NFC Push TX feature</strong>. The complete URL should look something like this (but longer):
|
||||
</p>
|
||||
|
||||
<p><code>https://coldcard.com/pushtx#t=AgAAAAMNCxXtp2GVYVhkRXHLMmdZFs4p3kbFK ⋯ ABf&c=uiSVRda-1tw</code></p>
|
||||
`;
|
||||
|
||||
messageArea.innerHTML = renderMessage('info', msg);
|
||||
return;
|
||||
}
|
||||
|
||||
messageArea.innerHTML = renderMessage('progress', 'Sending transaction, please wait...');
|
||||
|
||||
const [parseErr, parseResult] = await collect(parseFragment(window.location.hash));
|
||||
|
||||
if (parseErr) {
|
||||
messageArea.innerHTML = renderMessage('error', parseErr.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const { tx, network } = parseResult;
|
||||
|
||||
const txid = tx.getId();
|
||||
|
||||
const [pushErr, pushResult] = await collect(pushTx(tx, network));
|
||||
|
||||
// XXX - sometimes we get something like `{"code":-25,"message":"bad-txns-inputs-missingorspent"}`
|
||||
// but the transaction is actually confirmed, so:
|
||||
// - try to fetch the TX details by ID, even if pushing failed
|
||||
// - if it's found, show a green message and say it's pending or confirmed
|
||||
const [mempoolErr, mempoolTxData] = await collect(fetchTxData(txid, network));
|
||||
|
||||
if (pushResult || mempoolTxData) {
|
||||
// push was successful and/or we got the TX details back
|
||||
const msg = mempoolTxData?.status.confirmed
|
||||
? 'This transaction has already been confirmed.'
|
||||
: 'The transaction has been sent and is waiting to be confirmed.';
|
||||
|
||||
const listHtml = BLOCK_EXPLORER_CONFIG[network]
|
||||
.map(
|
||||
([name, url]) =>
|
||||
`<li><a href="${url}${txid}" target="_blank" rel="noopener">${name}</a></li>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
messageArea.innerHTML = renderMessage(
|
||||
'success',
|
||||
`<p>${msg} Transaction ID:</p>
|
||||
|
||||
<p class="txid">${txid}</p>
|
||||
|
||||
<p>Verify on a block explorer:</p>
|
||||
|
||||
<ul>${listHtml}</ul>
|
||||
`
|
||||
);
|
||||
|
||||
if (mempoolTxData) {
|
||||
detailsArea.innerHTML = renderTxDetails(mempoolTxData);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (pushErr && mempoolErr) {
|
||||
// pushing failed and we also couldn't fetch the TX details
|
||||
messageArea.innerHTML = renderMessage('error', pushErr.message);
|
||||
} else if (mempoolErr) {
|
||||
messageArea.innerHTML = renderMessage('error', mempoolErr.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
window.addEventListener('hashchange', run);
|
||||
@@ -0,0 +1,130 @@
|
||||
.pushtx * {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.pushtx ul {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.pushtx .monospace {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.pushtx-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.pushtx-message {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pushtx-message > :first-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pushtx-message > :last-child {
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pushtx-message--success {
|
||||
color: #155724;
|
||||
background-color: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
}
|
||||
|
||||
.pushtx-message--info {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
.pushtx-message--progress {
|
||||
color: #004085;
|
||||
background-color: #cce5ff;
|
||||
border-color: #b8daff;
|
||||
}
|
||||
|
||||
.pushtx-message--error {
|
||||
color: #721c24;
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
|
||||
.pushtx-message .txid {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
font-size: 0.875em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pushtx-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-block: 2rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.pushtx-details table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pushtx-details th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pushtx-details :is(th, td) {
|
||||
border-width: 0px;
|
||||
padding-block: 0.125rem;
|
||||
}
|
||||
|
||||
.pushtx-details :is(td:first-child, th:first-child) {
|
||||
width: 75%;
|
||||
max-width: 0px;
|
||||
}
|
||||
|
||||
.pushtx-details :is(td:last-child, th:last-child) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.pushtx-details :is(.address, .value) {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.pushtx-details .address {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.pushtx-details .address :first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pushtx-details .fee {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export type Network = 'BTC' | 'XTN' | 'XRT';
|
||||
|
||||
interface MempoolConfirmedStatus {
|
||||
confirmed: true;
|
||||
block_height: number;
|
||||
}
|
||||
|
||||
interface MempoolUnconfirmedStatus {
|
||||
confirmed: false;
|
||||
}
|
||||
|
||||
export interface MempoolTxData {
|
||||
vin: {
|
||||
prevout: {
|
||||
scriptpubkey_address: string;
|
||||
value: number;
|
||||
};
|
||||
}[];
|
||||
vout: {
|
||||
scriptpubkey_address: string;
|
||||
value: number;
|
||||
}[];
|
||||
fee: number;
|
||||
status: MempoolConfirmedStatus | MempoolUnconfirmedStatus;
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { defineConfig, type UserConfig } from 'vite';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||
|
||||
const plugins = [
|
||||
nodePolyfills({
|
||||
// needed for bitcoinjs-lib
|
||||
include: ['buffer'],
|
||||
globals: {
|
||||
Buffer: true,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
// "normal" configuration - building for coldcard.com/pushtx
|
||||
// - creates a one JS and one CSS file
|
||||
const websiteConfig: UserConfig = {
|
||||
plugins,
|
||||
build: {
|
||||
outDir: 'build',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: (chunkInfo) => {
|
||||
if (chunkInfo.name !== 'index') {
|
||||
throw new Error(
|
||||
`Build failed. Expected a single JS chunk "index". Saw: "${chunkInfo.name}".`
|
||||
);
|
||||
}
|
||||
return 'pushtx.js';
|
||||
},
|
||||
assetFileNames: (chunkInfo) => {
|
||||
if (chunkInfo.name !== 'index.css') {
|
||||
throw new Error(
|
||||
`Build failed. Expected a single CSS chunk "index.css". Saw: "${chunkInfo.name}".`
|
||||
);
|
||||
}
|
||||
return 'pushtx.css';
|
||||
},
|
||||
chunkFileNames: (chunkInfo) => {
|
||||
throw new Error(`Build failed. Not expecting any chunk files. Saw: "${chunkInfo.name}".`);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// single file config - builds a single HTML file with everything inlined
|
||||
// can be downloaded from coldcard.com/pushtx to self-host
|
||||
const singleFileConfig: UserConfig = {
|
||||
plugins: [...plugins, viteSingleFile()],
|
||||
build: {
|
||||
outDir: 'build-single-file',
|
||||
},
|
||||
};
|
||||
|
||||
const finalConfig = process.env.PUSHTX_SINGLE_FILE ? singleFileConfig : websiteConfig;
|
||||
|
||||
export default defineConfig(finalConfig);
|
||||
Reference in new issue
Block a user