Merge branch 'master' into mononaut/pool-reindexing
This commit is contained in:
		
						commit
						cf09669902
					
				| @ -331,5 +331,270 @@ | ||||
|             "block_hash": "00000000000000000002c69c7a3010fcd596c0c7451c23e7cd1f5e19ebf8ee6d", | ||||
|             "block_time": 1718517071 | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         "txid": "b10c0000004da5a9d1d9b4ae32e09f0b3e62d21a5cce5428d4ad714fb444eb5d", | ||||
|         "version": 1, | ||||
|         "locktime": 1231006505, | ||||
|         "vin": [ | ||||
|             { | ||||
|                 "txid": "d46a24962c1d7bd6e87d80570c6a53413eaf30d7fde7f52347f13645ae53969b", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "41049434a2dd7c5b82df88f578f8d7fd14e8d36513aaa9c003eb5bd6cb56065e44b7e0227139e8a8e68e7de0a4ed32b8c90edc9673b8a7ea541b52f2a22196f7b8cfac", | ||||
|                     "scriptpubkey_asm": "OP_PUSHBYTES_65 049434a2dd7c5b82df88f578f8d7fd14e8d36513aaa9c003eb5bd6cb56065e44b7e0227139e8a8e68e7de0a4ed32b8c90edc9673b8a7ea541b52f2a22196f7b8cf OP_CHECKSIG", | ||||
|                     "scriptpubkey_type": "p2pk", | ||||
|                     "value": 6102 | ||||
|                 }, | ||||
|                 "scriptsig": "473044022004f027ae0b19bb7a7aa8fcdf135f1da769d087342020359ef4099a9f0f0ba4ec02206a83a9b78df3fed89a3b6052e69963e1fb08d8f6d17d945e43b51b5214aa41e601", | ||||
|                 "scriptsig_asm": "OP_PUSHBYTES_71 3044022004f027ae0b19bb7a7aa8fcdf135f1da769d087342020359ef4099a9f0f0ba4ec02206a83a9b78df3fed89a3b6052e69963e1fb08d8f6d17d945e43b51b5214aa41e601", | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 20090103 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "76a914bbb1f7d0f7e15ac088af9bafe25aaac1a59832d088ac", | ||||
|                     "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 bbb1f7d0f7e15ac088af9bafe25aaac1a59832d0 OP_EQUALVERIFY OP_CHECKSIG", | ||||
|                     "scriptpubkey_type": "p2pkh", | ||||
|                     "scriptpubkey_address": "1J7SZJry7CX4zWdH3P8E8UJjZrhcLEjJ39", | ||||
|                     "value": 1913 | ||||
|                 }, | ||||
|                 "scriptsig": "46304302204dc2939be89ab6626457fff40aec2cc4e6213e64bcb4d2c43bf6b49358ff638c021f33d2f8fdf6d54a2c82bb7cddc62becc2cbbaca6fd7f3ec927ea975f29ad8510221028b98707adfd6f468d56c1a6067a6f0c7fef43afbacad45384017f8be93a18d40", | ||||
|                 "scriptsig_asm": "OP_PUSHBYTES_70 304302204dc2939be89ab6626457fff40aec2cc4e6213e64bcb4d2c43bf6b49358ff638c021f33d2f8fdf6d54a2c82bb7cddc62becc2cbbaca6fd7f3ec927ea975f29ad85102 OP_PUSHBYTES_33 028b98707adfd6f468d56c1a6067a6f0c7fef43afbacad45384017f8be93a18d40", | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 20081031 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3", | ||||
|                 "vout": 1, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "52210304e708d258a632ffb128a62ecf5eebd1904e505497d031619513afc8bca7858f2102b9dc03f1133e7cbc7eb311631acc2dbda908fb0f0fae095da2f4dd427f51308a4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f53ae", | ||||
|                     "scriptpubkey_asm": "OP_PUSHNUM_2 OP_PUSHBYTES_33 0304e708d258a632ffb128a62ecf5eebd1904e505497d031619513afc8bca7858f OP_PUSHBYTES_33 02b9dc03f1133e7cbc7eb311631acc2dbda908fb0f0fae095da2f4dd427f51308a OP_PUSHBYTES_65 04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f OP_PUSHNUM_3 OP_CHECKMULTISIG", | ||||
|                     "scriptpubkey_type": "multisig", | ||||
|                     "value": 1971 | ||||
|                 }, | ||||
|                 "scriptsig": "00453042021e4f6ff73d7b304a5cbf3bb7738abb5f81a4af6335962134ce27a1cc45fec702201b95e3acb7db93257b20651cdcb79af66bf0bb86a8ae5b4e0a5df4e3f86787e2033b303802153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021f34793e2878497561e7616291ebdda3024b681cdacc8b863b5b0804cd30c2a481", | ||||
|                 "scriptsig_asm": "OP_0 OP_PUSHBYTES_69 3042021e4f6ff73d7b304a5cbf3bb7738abb5f81a4af6335962134ce27a1cc45fec702201b95e3acb7db93257b20651cdcb79af66bf0bb86a8ae5b4e0a5df4e3f86787e203 OP_PUSHBYTES_59 303802153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021f34793e2878497561e7616291ebdda3024b681cdacc8b863b5b0804cd30c2a481", | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 19750504 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "45e1cb33599acb071810ccc801b71bd7610865f5b899492946ab1bfbcb61cad6", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "a91419f0b86f61606c6eb51b217698ca7e8bff1e398b87", | ||||
|                     "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 19f0b86f61606c6eb51b217698ca7e8bff1e398b OP_EQUAL", | ||||
|                     "scriptpubkey_type": "p2sh", | ||||
|                     "scriptpubkey_address": "344BBtYkhaCXgA7oYSXASUfh4bFieiponG", | ||||
|                     "value": 2140 | ||||
|                 }, | ||||
|                 "scriptsig": "00443041021d1313459a48bd1d0628eec635495f793e970729684394f9b814d2b24012022050be6d9918444e283da0136884f8311ec465d0fed2f8d24b75a8485ebdc13aea013a303702153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021e78644ba72eab69fefb5fe50700671bfb91dda699f72ffbb325edc6a3c4ef8239303602153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021d2c2db104e70720c39af43b6ba3edd930c26e0818aa59ff9c886281d8ba834ced532103e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc2103cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd421027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a34104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c55ae", | ||||
|                 "scriptsig_asm": "OP_0 OP_PUSHBYTES_68 3041021d1313459a48bd1d0628eec635495f793e970729684394f9b814d2b24012022050be6d9918444e283da0136884f8311ec465d0fed2f8d24b75a8485ebdc13aea01 OP_PUSHBYTES_58 303702153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021e78644ba72eab69fefb5fe50700671bfb91dda699f72ffbb325edc6a3c4ef82 OP_PUSHBYTES_57 303602153b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63021d2c2db104e70720c39af43b6ba3edd930c26e0818aa59ff9c886281d8ba83 OP_PUSHDATA1 532103e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc2103cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd421027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a34104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c55ae", | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 16, | ||||
|                 "inner_redeemscript_asm": "OP_PUSHNUM_3 OP_PUSHBYTES_33 03e0a220d36f6f7ed5f3f58c279d055707c454135baf18fd00d798fec3cb52dfbc OP_PUSHBYTES_33 03cf689db9313b9f7fc0b984dd9cac750be76041b392919b06f6bf94813da34cd4 OP_PUSHBYTES_33 027f8af2eb6e904deddaa60d5af393d430575eb35e4dfd942a8a5882734b078906 OP_PUSHBYTES_65 0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3 OP_PUSHBYTES_65 04ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c OP_PUSHNUM_5 OP_CHECKMULTISIG" | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3", | ||||
|                 "vout": 2, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "a9143b13a1f71c20c799d86bb624b3898c826d6c82da87", | ||||
|                     "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 3b13a1f71c20c799d86bb624b3898c826d6c82da OP_EQUAL", | ||||
|                     "scriptpubkey_type": "p2sh", | ||||
|                     "scriptpubkey_address": "375PJxsKRtAq4WoS6u82jvgZW94R8Wx3iH", | ||||
|                     "value": 5139 | ||||
|                 }, | ||||
|                 "scriptsig": "1600149b27f072e4b972927c445d1946162a550b0914d8", | ||||
|                 "scriptsig_asm": "OP_PUSHBYTES_22 00149b27f072e4b972927c445d1946162a550b0914d8", | ||||
|                 "witness": [ | ||||
|                     "3040021c23902a01d4c5cff2c33c8bdb778a5aadea78a9a0d6d4db60aaa0fba1022069237d9dbf2db8cff9c260ba71250493682d01a746f4a45c5c7ea386e56d2bc902", | ||||
|                     "0240187acd3e2fd3d8e1acffefa85907b6550730c24f78dfd3301c829fc4daf3cc" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 141, | ||||
|                 "inner_redeemscript_asm": "OP_0 OP_PUSHBYTES_20 9b27f072e4b972927c445d1946162a550b0914d8" | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3", | ||||
|                 "vout": 3, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "a914a3c0698f2300c7b2e8107d4c9c988e642110039087", | ||||
|                     "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 a3c0698f2300c7b2e8107d4c9c988e6421100390 OP_EQUAL", | ||||
|                     "scriptpubkey_type": "p2sh", | ||||
|                     "scriptpubkey_address": "3GcrZrbUuvE4UtUdSbKTXcRnTqmfMdyMAC", | ||||
|                     "value": 3220 | ||||
|                 }, | ||||
|                 "scriptsig": "220020a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75", | ||||
|                 "scriptsig_asm": "OP_PUSHBYTES_34 0020a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75", | ||||
|                 "witness": [ | ||||
|                     "303f021c65aee6696e80be6e14545cfd64b44f17b0514c150eefdb090c0f0bd9021f3fef4aa95c252a225622aba99e4d5af5a6fe40d177acd593e64cf2f8557ccc03", | ||||
|                     "03b55c6f0749e0f3e2caeca05f68e3699f1b3c62a550730f704985a6a9aae437a1", | ||||
|                     "76a914db865fd920959506111079995f1e4017b489bfe38763ac6721024d560f7f5d28aae5e1a8aa2b7ba615d7fc48e4ea27e5d27336e6a8f5fa0f5c8c7c820120876475527c2103443e8834fa7d79d7b5e95e0e9d0847f6b03ac3ea977979858b4104947fca87ca52ae67a91446c3747322b220fdb925c9802f0e949c1feab99988ac6868" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 3735928559, | ||||
|                 "inner_redeemscript_asm": "OP_0 OP_PUSHBYTES_32 a18160de7291554f349c7d5cbee4ab97fb542e94cf302ce8d7e9747e4188ca75", | ||||
|                 "inner_witnessscript_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 db865fd920959506111079995f1e4017b489bfe3 OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 024d560f7f5d28aae5e1a8aa2b7ba615d7fc48e4ea27e5d27336e6a8f5fa0f5c8c OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 03443e8834fa7d79d7b5e95e0e9d0847f6b03ac3ea977979858b4104947fca87ca OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 46c3747322b220fdb925c9802f0e949c1feab999 OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF OP_ENDIF" | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "cb9b47ac04023b29fb633a8ef04af351ac9fd74c57c9a2163f683516274767e3", | ||||
|                 "vout": 4, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "0014c0ca6e754e65d3ba59112d7abc33e500c00ecfa7", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 c0ca6e754e65d3ba59112d7abc33e500c00ecfa7", | ||||
|                     "scriptpubkey_type": "v0_p2wpkh", | ||||
|                     "scriptpubkey_address": "bc1qcr9xua2wvhfm5kg394atcvl9qrqqana8rrmy8h", | ||||
|                     "value": 17144 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "303e021c11f60486afd0f5d6573603fb2076ef2f676455b92ada257d2f25558a021e317719c946f951d49bf4df4285a618629cd9e554fcbf787c319a0c4dd22601", | ||||
|                     "032467f24cc31664f0cf34ff8d5cbb590888ddc1dcfec724a32ae3dd5338b8508e" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 21000000 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "637db3928a8fb1b22b81f92dc738ee7637e5b172d650363d0b327429578bd001", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "0020a9530a167fcada672c142ee636dcd171796e69ef8e37aa1f77f35c58edd7a357", | ||||
|                     "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 a9530a167fcada672c142ee636dcd171796e69ef8e37aa1f77f35c58edd7a357", | ||||
|                     "scriptpubkey_type": "v0_p2wsh", | ||||
|                     "scriptpubkey_address": "bc1q49fs59nletdxwtq59mnrdhx3w9uku6003cm658mh7dw93mwh5dts2w2kht", | ||||
|                     "value": 8149 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "303d021c32f9454db85cb1a4ca63a9883d4347c5e13f3654e884ae44e9efa3c8021d62f07fe452c06b084bc3e09afd3aac4039136549a465533bc1ca66967902", | ||||
|                     "01", | ||||
|                     "632102fd6db4de50399b2aa086edb23f8e140bbc823d6651e024a0eb871288068789cd67012ab27521034134a2bb35c3f83dab2489d96160741888b8b5589bb694dea6e7bc24486e9c6f68ac" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 4190024921, | ||||
|                 "inner_witnessscript_asm": "OP_IF OP_PUSHBYTES_33 02fd6db4de50399b2aa086edb23f8e140bbc823d6651e024a0eb871288068789cd OP_ELSE OP_PUSHBYTES_1 2a OP_CSV OP_DROP OP_PUSHBYTES_33 034134a2bb35c3f83dab2489d96160741888b8b5589bb694dea6e7bc24486e9c6f OP_ENDIF OP_CHECKSIG" | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "0020db02df125062ebae5bacd189ebff22577b2817c1872be79a0d3ba3982c41", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "512071212ded0ff4c9b1b0c505d8012772e2dbe98a3cae7168377b950fb6b866a849", | ||||
|                     "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 71212ded0ff4c9b1b0c505d8012772e2dbe98a3cae7168377b950fb6b866a849", | ||||
|                     "scriptpubkey_type": "v1_p2tr", | ||||
|                     "scriptpubkey_address": "bc1pwysjmmg07nymrvx9qhvqzfmjutd7nz3u4ecksdmmj58mdwrx4pysq6m68g", | ||||
|                     "value": 9001 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "d822f203827852998cad370232e8c57294540a5da51107fa26cf466bdd2b8b0b3d161999cc80aed8de7386a2bd5d5313aea159a231cc26fa53aaa702b7fa21ed" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 341 | ||||
|             }, | ||||
|             { | ||||
|                 "txid": "795741ecf9c431b14b1c8d2dd017d3978fd4f6452e91edf416f31ef9971206b4", | ||||
|                 "vout": 0, | ||||
|                 "prevout": { | ||||
|                     "scriptpubkey": "512089ac120a490eee88db5588112f95f88093284c814f07c3ad943a7faefba2271a", | ||||
|                     "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 89ac120a490eee88db5588112f95f88093284c814f07c3ad943a7faefba2271a", | ||||
|                     "scriptpubkey_type": "v1_p2tr", | ||||
|                     "scriptpubkey_address": "bc1p3xkpyzjfpmhg3k643qgjl90cszfjsnypfuru8tv58fl6a7azyudqkcu66k", | ||||
|                     "value": 19953 | ||||
|                 }, | ||||
|                 "scriptsig": "", | ||||
|                 "scriptsig_asm": "", | ||||
|                 "witness": [ | ||||
|                     "fe6eb715dceffefc067fdc787d250a9a9116682d216f6356ea38fc1f112bd74995faa90315e81981d2c2260b7eaca3c41a16b280362980f0d8faf4c05ebb82c5", | ||||
|                     "e34ad0ad33885a473831f8ba8d9339123cb19d0e642e156d8e0d6e2ab2691aedb30e55a35637a806927225e1aa72223d41e59f92c6579b819e7d331a7ada9d2e01", | ||||
|                     "2a4861fb4cb951c791bf6c93859ef65abccd90034f91b9b77abb918e13b6fce75d5fa3e2d2f6eeeae105315178c2cb9db2ef238fe89b282f691c06db43bc71ca02", | ||||
|                     "fc97bb2be673c3bf388aaf58178ef14d354caf83c92aca8ef1831d619b8511e928f4f5fdea3962067b11e7cecfe094cd0f66a4ea9af9ec836d70d18f2b37df0281", | ||||
|                     "a5781a0adaa80ab7f7f164172dd1a1cb127e523daa0d6949aba074a15c589f12dfb8183182afec9230cb7947b7422a4abc1bb78173550d66274ea19f6c9dd92c82", | ||||
|                     "", | ||||
|                     "", | ||||
|                     "205f4237bd7dae576b34abc8a9c6fa4f0e4787c04234ca963e9e96c8f9b67b56d1ac205f4237bd7f93c69403a30c6b641f27ccf5201090152fcf1596474221307831c3ba205ac8ff25ce63564963d1148b84627f614af1f3c77d7caa23adc61264fa5e4996ba20b210c83e6f5b3f866837112d023d9ae8da2a6412168d54968ab87860ab970690ba20d3ee3b7a8b8149122b3c886330b3241538ba4b935c4040f4a73ddab917241bc5ba20cdfabb9d0e5c8f09a83f19e36e100d8f5e882f1b60aa60dacd9e6d072c117bc0ba20aab038c238e95fb54cdd0a6705dc1b1f8d135a9e9b20ab9c7ff96eef0e9bf545ba559c", | ||||
|                     "c0b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f5534a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33bf4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e166f7cf9580f1c2dfb3c4d5d043cdbb128c640e3f20161245aa7372e9666168516a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48dd5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb46829a3efd3ef04f9153d47a990bd7b048a4b2d213daaa5fb8ed670fb85f13bdbcf54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713d29c9c0e8e4d2a9790922af73f0b8d51f0bd4bb19940d9cf910ead8fbe85bc9bbb41a757f405890fb0f5856228e23b715702d714d59bf2b1feb70d8b2b4e3e089fdbcf0ef9d8d00f66e47917f67cc5d78aec1ac786e2abb8d2facb4e4790aad6cc455ae816e6cdafdb58d54e35d4f46d860047458eacf1c7405dc634631c570d8d31992805518fd62daa3bdd2a5c4fd2cd3054c9b3dca1d78055e9528cff6adc8f907925d2ebe48765103e6845c06f1f2bb77c6adc1cc002865865eb5cfd5c1cb10c007c60e14f9d087e0291d4d0c7869697c6681d979c6639dbd960792b4d4133e794d097969002ee05d336686fc03c9e15a597c1b9827669460fac9879903637777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8fd456524104a6674693c29946543f8a0befccce5a352bda55ec8559fc630f5f37393096d97bfee8660f4100ffd61874d62f9a65de9fb6acf740c4c386990ef7373be398c4bdc43709db7398106609eea2a7841aaf3a4fa2000dc18184faa2a7eb5a2af5845a8d3796308ff9840e567b14cf6bb158ff26c999e6f9a1f5448f9aa" | ||||
|                 ], | ||||
|                 "is_coinbase": false, | ||||
|                 "sequence": 342, | ||||
|                 "inner_witnessscript_asm": "OP_PUSHBYTES_32 5f4237bd7dae576b34abc8a9c6fa4f0e4787c04234ca963e9e96c8f9b67b56d1 OP_CHECKSIG OP_PUSHBYTES_32 5f4237bd7f93c69403a30c6b641f27ccf5201090152fcf1596474221307831c3 OP_CHECKSIGADD OP_PUSHBYTES_32 5ac8ff25ce63564963d1148b84627f614af1f3c77d7caa23adc61264fa5e4996 OP_CHECKSIGADD OP_PUSHBYTES_32 b210c83e6f5b3f866837112d023d9ae8da2a6412168d54968ab87860ab970690 OP_CHECKSIGADD OP_PUSHBYTES_32 d3ee3b7a8b8149122b3c886330b3241538ba4b935c4040f4a73ddab917241bc5 OP_CHECKSIGADD OP_PUSHBYTES_32 cdfabb9d0e5c8f09a83f19e36e100d8f5e882f1b60aa60dacd9e6d072c117bc0 OP_CHECKSIGADD OP_PUSHBYTES_32 aab038c238e95fb54cdd0a6705dc1b1f8d135a9e9b20ab9c7ff96eef0e9bf545 OP_CHECKSIGADD OP_PUSHNUM_5 OP_NUMEQUAL" | ||||
|             } | ||||
|         ], | ||||
|         "vout": [ | ||||
|             { | ||||
|                 "scriptpubkey": "210261542eb020b36c1da48e2e607b90a8c1f2ccdbd06eaf5fb4bb0d7cc34293d32aac", | ||||
|                 "scriptpubkey_asm": "OP_PUSHBYTES_33 0261542eb020b36c1da48e2e607b90a8c1f2ccdbd06eaf5fb4bb0d7cc34293d32a OP_CHECKSIG", | ||||
|                 "scriptpubkey_type": "p2pk", | ||||
|                 "value": 576 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "76a9140240539af6c68431e4ce9cc5ef464f12c1741b3c88ac", | ||||
|                 "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 0240539af6c68431e4ce9cc5ef464f12c1741b3c OP_EQUALVERIFY OP_CHECKSIG", | ||||
|                 "scriptpubkey_type": "p2pkh", | ||||
|                 "scriptpubkey_address": "1CuQsdrcgcmPvugo3NqEwh1kDcpeEnuFC", | ||||
|                 "value": 546 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "5121028b45a50f795be0413680036665d17a3eca099648ea80637bc3a70a7d2b52ae2851ae", | ||||
|                 "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_33 028b45a50f795be0413680036665d17a3eca099648ea80637bc3a70a7d2b52ae28 OP_PUSHNUM_1 OP_CHECKMULTISIG", | ||||
|                 "scriptpubkey_type": "multisig", | ||||
|                 "value": 582 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "a91449ed2c96e33b6134408af8484508bcc3248c8dbd87", | ||||
|                 "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 49ed2c96e33b6134408af8484508bcc3248c8dbd OP_EQUAL", | ||||
|                 "scriptpubkey_type": "p2sh", | ||||
|                 "scriptpubkey_address": "38RuNhSiZiftB6WVnStu5aUz6jXtCDXQZk", | ||||
|                 "value": 540 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "0014c8e51cf6891c0a2101aecea8cd5ce9bbbfaf7bba", | ||||
|                 "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 c8e51cf6891c0a2101aecea8cd5ce9bbbfaf7bba", | ||||
|                 "scriptpubkey_type": "v0_p2wpkh", | ||||
|                 "scriptpubkey_address": "bc1qerj3ea5frs9zzqdwe65v6h8fhwl677a6s0hxhf", | ||||
|                 "value": 294 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "0020c485bbb80c4be276e77eac3a983a391cc8b1a1b5f160995a36c3dff18296385a", | ||||
|                 "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_32 c485bbb80c4be276e77eac3a983a391cc8b1a1b5f160995a36c3dff18296385a", | ||||
|                 "scriptpubkey_type": "v0_p2wsh", | ||||
|                 "scriptpubkey_address": "bc1qcjzmhwqvf038dem74safsw3ernytrgd479sfjk3kc00lrq5k8pdqczl83q", | ||||
|                 "value": 330 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "5120a7a42b268957a06c9de4d7260f1df392ce4d6e7b743f5adc27415ce2afceb3b9", | ||||
|                 "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 a7a42b268957a06c9de4d7260f1df392ce4d6e7b743f5adc27415ce2afceb3b9", | ||||
|                 "scriptpubkey_type": "v1_p2tr", | ||||
|                 "scriptpubkey_address": "bc1p57jzkf5f27sxe80y6unq780njt8y6mnmwsl44hp8g9ww9t7wkwusv7av76", | ||||
|                 "value": 330 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "51024e73", | ||||
|                 "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_2 4e73", | ||||
|                 "scriptpubkey_type": "unknown", | ||||
|                 "scriptpubkey_address": "bc1pfeessrawgf", | ||||
|                 "value": 240 | ||||
|             }, | ||||
|             { | ||||
|                 "scriptpubkey": "6a224e6f7420796f757220696e707574732c206e6f7420796f7572206f7574707574732e005152535455565758595a5b5c5d5e5f60", | ||||
|                 "scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_34 4e6f7420796f757220696e707574732c206e6f7420796f7572206f7574707574732e OP_0 OP_PUSHNUM_1 OP_PUSHNUM_2 OP_PUSHNUM_3 OP_PUSHNUM_4 OP_PUSHNUM_5 OP_PUSHNUM_6 OP_PUSHNUM_7 OP_PUSHNUM_8 OP_PUSHNUM_9 OP_PUSHNUM_10 OP_PUSHNUM_11 OP_PUSHNUM_12 OP_PUSHNUM_13 OP_PUSHNUM_14 OP_PUSHNUM_15 OP_PUSHNUM_16", | ||||
|                 "scriptpubkey_type": "op_return", | ||||
|                 "value": 0 | ||||
|             } | ||||
|         ], | ||||
|         "size": 3500, | ||||
|         "weight": 8186, | ||||
|         "sigops": 115, | ||||
|         "fee": 71294, | ||||
|         "status": { | ||||
|             "confirmed": true, | ||||
|             "block_height": 850000, | ||||
|             "block_hash": "00000000000000000002a0b5db2a7f8d9087464c2586b546be7bce8eb53b8187", | ||||
|             "block_time": 1719689674 | ||||
|         } | ||||
|     } | ||||
| ] | ||||
| @ -14,6 +14,7 @@ class AccelerationRoutes { | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this)) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this)) | ||||
|       .get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this)) | ||||
|       .post(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/estimate', this.$getAcceleratorEstimate.bind(this)) | ||||
|     ; | ||||
|   } | ||||
| 
 | ||||
| @ -64,6 +65,20 @@ class AccelerationRoutes { | ||||
|       res.status(500).end(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async $getAcceleratorEstimate(req: Request, res: Response): Promise<void> { | ||||
|     const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`; | ||||
|     try { | ||||
|       const response = await axios.post(url, req.body, { responseType: 'stream', timeout: 10000 }); | ||||
|       for (const key in response.headers) { | ||||
|         res.setHeader(key, response.headers[key]); | ||||
|       } | ||||
|       response.data.pipe(res); | ||||
|     } catch (e) { | ||||
|       logger.err(`Unable to get acceleration estimate from ${url} in $getAcceleratorEstimate(), ${e}`, this.tag); | ||||
|       res.status(500).end(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default new AccelerationRoutes(); | ||||
| @ -165,13 +165,21 @@ class BitcoinApi implements AbstractBitcoinApi { | ||||
|     const mp = mempool.getMempool(); | ||||
|     for (const tx in mp) { | ||||
|       for (const vout of mp[tx].vout) { | ||||
|         if (vout.scriptpubkey_address.indexOf(prefix) === 0) { | ||||
|         if (vout.scriptpubkey_address?.indexOf(prefix) === 0) { | ||||
|           found[vout.scriptpubkey_address] = ''; | ||||
|           if (Object.keys(found).length >= 10) { | ||||
|             return Object.keys(found); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       for (const vin of mp[tx].vin) { | ||||
|         if (vin.prevout?.scriptpubkey_address?.indexOf(prefix) === 0) { | ||||
|           found[vin.prevout?.scriptpubkey_address] = ''; | ||||
|           if (Object.keys(found).length >= 10) { | ||||
|             return Object.keys(found); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return Object.keys(found); | ||||
|   } | ||||
|  | ||||
| @ -54,7 +54,7 @@ export namespace IEsploraApi { | ||||
|     scriptpubkey: string; | ||||
|     scriptpubkey_asm: string; | ||||
|     scriptpubkey_type: string; | ||||
|     scriptpubkey_address: string; | ||||
|     scriptpubkey_address?: string; | ||||
|     value: number; | ||||
|     // Elements
 | ||||
|     valuecommitment?: number; | ||||
|  | ||||
| @ -706,7 +706,7 @@ class Blocks { | ||||
|         } | ||||
| 
 | ||||
|         const coinbaseTx = await bitcoinApi.$getCoinbaseTx(hash); | ||||
|         const addresses = new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a)); | ||||
|         const addresses = new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[]); | ||||
|         await blocksRepository.$saveCoinbaseAddresses(hash, [...addresses]); | ||||
| 
 | ||||
|         // Logging
 | ||||
|  | ||||
| @ -292,7 +292,7 @@ export class Common { | ||||
|         dustSize += getVarIntLength(dustSize); | ||||
|         // add value size
 | ||||
|         dustSize += 8; | ||||
|         if (['v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) { | ||||
|         if (Common.isWitnessProgram(vout.scriptpubkey)) { | ||||
|           dustSize += 67; | ||||
|         } else { | ||||
|           dustSize += 148; | ||||
| @ -460,11 +460,10 @@ export class Common { | ||||
|           case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break; | ||||
|           case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break; | ||||
|           case 'v1_p2tr': { | ||||
|             if (!vin.witness?.length) { | ||||
|               throw new Error('Taproot input missing witness data'); | ||||
|             } | ||||
|             flags |= TransactionFlags.p2tr; | ||||
|             flags = Common.isInscription(vin, flags); | ||||
|             if (vin.witness?.length) { | ||||
|               flags = Common.isInscription(vin, flags); | ||||
|             } | ||||
|           } break; | ||||
|         } | ||||
|       } else { | ||||
|  | ||||
| @ -18,6 +18,7 @@ fi | ||||
| 
 | ||||
| __MAINNET_ENABLED__=${MAINNET_ENABLED:=true} | ||||
| __TESTNET_ENABLED__=${TESTNET_ENABLED:=false} | ||||
| __TESTNET4_ENABLED__=${TESTNET_ENABLED:=false} | ||||
| __SIGNET_ENABLED__=${SIGNET_ENABLED:=false} | ||||
| __LIQUID_ENABLED__=${LIQUID_ENABLED:=false} | ||||
| __LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false} | ||||
| @ -46,6 +47,7 @@ __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} | ||||
| # Export as environment variables to be used by envsubst | ||||
| export __MAINNET_ENABLED__ | ||||
| export __TESTNET_ENABLED__ | ||||
| export __TESTNET4_ENABLED__ | ||||
| export __SIGNET_ENABLED__ | ||||
| export __LIQUID_ENABLED__ | ||||
| export __LIQUID_TESTNET_ENABLED__ | ||||
|  | ||||
| @ -146,8 +146,9 @@ let routes: Routes = [ | ||||
|     data: { preload: true }, | ||||
|   }, | ||||
|   { | ||||
|     path: 'tracker/:id', | ||||
|     component: TrackerComponent, | ||||
|     path: 'tracker', | ||||
|     data: { networkSpecific: true }, | ||||
|     loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule), | ||||
|   }, | ||||
|   { | ||||
|     path: 'wallet', | ||||
|  | ||||
| @ -71,19 +71,24 @@ export function calcSegwitFeeGains(tx: Transaction) { | ||||
|     } | ||||
| 
 | ||||
|     if (isP2tr) { | ||||
|       if (vin.witness.length === 1) { | ||||
|         // key path spend
 | ||||
|         // we don't know if this was a multisig or single sig (the goal of taproot :)),
 | ||||
|         // so calculate fee savings by comparing to the cheapest single sig input type: P2WPKH and say "saved at least ...%"
 | ||||
|         // the witness size of P2WPKH is 1 (stack size) + 1 (size) + 72 (low s signature) + 1 (size) + 33 (pubkey) = 108 WU
 | ||||
|         // the witness size of key path P2TR is 1 (stack size) + 1 (size) + 64 (signature) = 66 WU
 | ||||
|         realizedTaprootGains += 42; | ||||
|       } else { | ||||
|         // script path spend
 | ||||
|         // complex scripts with multiple spending paths can often be made around 2x to 3x smaller with the Taproot script tree
 | ||||
|         // because only the hash of the alternative spending path has the be in the witness data, not the entire script,
 | ||||
|         // but only assumptions can be made because the scripts themselves are unknown (again, the goal of taproot :))
 | ||||
|         // TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts
 | ||||
|       // every valid taproot input has at least one witness item, however transactions
 | ||||
|       // created before taproot activation don't need to have any witness data
 | ||||
|       // (see https://mempool.space/tx/b10c007c60e14f9d087e0291d4d0c7869697c6681d979c6639dbd960792b4d41)
 | ||||
|       if (vin.witness?.length) { | ||||
|         if (vin.witness.length === 1) { | ||||
|           // key path spend
 | ||||
|           // we don't know if this was a multisig or single sig (the goal of taproot :)),
 | ||||
|           // so calculate fee savings by comparing to the cheapest single sig input type: P2WPKH and say "saved at least ...%"
 | ||||
|           // the witness size of P2WPKH is 1 (stack size) + 1 (size) + 72 (low s signature) + 1 (size) + 33 (pubkey) = 108 WU
 | ||||
|           // the witness size of key path P2TR is 1 (stack size) + 1 (size) + 64 (signature) = 66 WU
 | ||||
|           realizedTaprootGains += 42; | ||||
|         } else { | ||||
|           // script path spend
 | ||||
|           // complex scripts with multiple spending paths can often be made around 2x to 3x smaller with the Taproot script tree
 | ||||
|           // because only the hash of the alternative spending path has the be in the witness data, not the entire script,
 | ||||
|           // but only assumptions can be made because the scripts themselves are unknown (again, the goal of taproot :))
 | ||||
|           // TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts
 | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       const script = isP2shP2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm; | ||||
|  | ||||
| @ -53,13 +53,26 @@ | ||||
|         <span>Spiral</span> | ||||
|       </a> | ||||
|       <a href="https://foundrydigital.com/" target="_blank" title="Foundry"> | ||||
|         <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-10 -10 100 100" class="image"> | ||||
|           <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||||
|             <g transform="translate(-186.000000, -2316.000000)"> | ||||
|               <g transform="translate(186.000000, 2316.000000)"> | ||||
|                 <rect id="" fill="#023D32" x="-10" y="-10" width="100" height="100" rx="8"></rect> | ||||
|                 <path d="M61.6666667,9.16666667 L61.6666667,17.0041667 L46.2625,17.0041667 C46.2625,17.0041667 44.1666667,16.6666667 44.1666667,18.3333333 L44.1666667,25.8025 L61.6666667,25.8025 L61.6666667,34.7391667 L44.1666667,34.7391667 L44.1666667,70.5575 L31.7825,70.5575 L31.7825,35 L19.1666667,35 L19.1666667,25.595 L31.6666667,25.595 L31.6666667,17.5 C31.6666667,17.5 32.5,9.16666667 40.4166667,9.16666667 L61.6666667,9.16666667 Z" id="Fill-1" fill="#86E2A0"></path> | ||||
|               </g> | ||||
|         <svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76"> | ||||
|           <defs> | ||||
|             <style> | ||||
|               .d { | ||||
|                 fill: #fff; | ||||
|               } | ||||
| 
 | ||||
|               .e { | ||||
|                 fill: #ff8200; | ||||
|               } | ||||
|             </style> | ||||
|           </defs> | ||||
|           <g id="c" data-name="b"> | ||||
|             <circle class="e" cx="24" cy="32" r="8" /> | ||||
|             <circle class="e" cx="24" cy="56" r="8" /> | ||||
|             <circle class="e" cx="8" cy="68" r="8" /> | ||||
|             <g> | ||||
|               <circle class="d" cx="24" cy="8" r="8" /> | ||||
|               <circle class="d" cx="8" cy="20" r="8" /> | ||||
|               <circle class="d" cx="8" cy="44" r="8" /> | ||||
|             </g> | ||||
|           </g> | ||||
|         </svg> | ||||
| @ -259,22 +272,10 @@ | ||||
|         <img class="image" src="/resources/profile/bisq_network.png" /> | ||||
|         <span>Bisq</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet"> | ||||
|         <img class="image" src="/resources/profile/bluewallet.png" /> | ||||
|         <span>BlueWallet</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet"> | ||||
|         <img class="image" src="/resources/profile/muun.png" /> | ||||
|         <span>Muun</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet"> | ||||
|         <img class="image" src="/resources/profile/electrum.png" /> | ||||
|         <span>Electrum</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet"> | ||||
|         <img class="image" src="/resources/profile/specter.png" /> | ||||
|         <span>Specter</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/sparrowwallet/sparrow" target="_blank" title="Sparrow Wallet"> | ||||
|         <img class="image" src="/resources/profile/sparrow.png" /> | ||||
|         <span>Sparrow</span> | ||||
| @ -283,21 +284,37 @@ | ||||
|         <img class="image not-rounded" src="/resources/profile/phoenix.svg" /> | ||||
|         <span>Phoenix</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits"> | ||||
|         <img class="image" src="/resources/profile/lnbits.svg" /> | ||||
|         <span>LNBits</span> | ||||
|       <a href="http://github.com/COLDCARD" target="_blank" title="COLDCARD"> | ||||
|         <img class="image coldcard" src="/resources/profile/coldcard.png" /> | ||||
|         <span>COLDCARD</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/layer2tech/mercury-wallet" target="_blank" title="Mercury Wallet"> | ||||
|         <img class="image" src="/resources/profile/mercury.svg" /> | ||||
|         <span>Mercury</span> | ||||
|       <a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS"> | ||||
|         <img class="image" src="/resources/profile/zeus.png" /> | ||||
|         <span>ZEUS</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny"> | ||||
|         <img class="image not-rounded" src="/resources/profile/mutiny.svg" /> | ||||
|         <span>Mutiny</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/hsjoberg/blixt-wallet" target="_blank" title="Blixt Wallet"> | ||||
|         <img class="image" src="/resources/profile/blixt.png" /> | ||||
|         <span>Blixt</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS"> | ||||
|         <img class="image" src="/resources/profile/zeus.png" /> | ||||
|         <span>ZEUS</span> | ||||
|       <a href="https://github.com/nunchuk-io" target="_blank" title="Nunchuck"> | ||||
|         <img class="image" src="/resources/profile/nunchuk.svg" /> | ||||
|         <span>Nunchuk</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet"> | ||||
|         <img class="image" src="/resources/profile/bluewallet.png" /> | ||||
|         <span>BlueWallet</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/BoltzExchange" target="_blank" title="Boltz"> | ||||
|         <img class="image" src="/resources/profile/boltz.svg" /> | ||||
|         <span>Boltz</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits"> | ||||
|         <img class="image" src="/resources/profile/lnbits.svg" /> | ||||
|         <span>LNBits</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet"> | ||||
|         <img class="image" src="/resources/profile/marina.svg" /> | ||||
| @ -307,13 +324,9 @@ | ||||
|         <img class="image" src="/resources/profile/schildbach.svg" /> | ||||
|         <span>Schildbach</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/nunchuk-io" target="_blank" title="Nunchuck"> | ||||
|         <img class="image" src="/resources/profile/nunchuk.svg" /> | ||||
|         <span>Nunchuk</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s"> | ||||
|         <img class="image" src="/resources/profile/bitcoin-s.svg" /> | ||||
|         <span>bitcoin-s</span> | ||||
|       <a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet"> | ||||
|         <img class="image" src="/resources/profile/specter.png" /> | ||||
|         <span>Specter</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/EdgeApp" target="_blank" title="Edge"> | ||||
|         <img class="image not-rounded" src="/resources/profile/edge.svg" /> | ||||
| @ -323,13 +336,13 @@ | ||||
|         <img class="image" src="/resources/profile/galoy.svg" /> | ||||
|         <span>Galoy</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/BoltzExchange" target="_blank" title="Boltz"> | ||||
|         <img class="image" src="/resources/profile/boltz.svg" /> | ||||
|         <span>Boltz</span> | ||||
|       <a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet"> | ||||
|         <img class="image" src="/resources/profile/muun.png" /> | ||||
|         <span>Muun</span> | ||||
|       </a> | ||||
|       <a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny"> | ||||
|         <img class="image not-rounded" src="/resources/profile/mutiny.svg" /> | ||||
|         <span>Mutiny</span> | ||||
|       <a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s"> | ||||
|         <img class="image" src="/resources/profile/bitcoin-s.svg" /> | ||||
|         <span>bitcoin-s</span> | ||||
|       </a> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
| @ -156,6 +156,12 @@ | ||||
|         } | ||||
|         img, svg { | ||||
|           margin: 40px 29px 10px; | ||||
|           &.image.coldcard { | ||||
|             border-radius: 0; | ||||
|             width: auto; | ||||
|             max-height: 50px; | ||||
|             margin: 40px 29px 14px 29px; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @ -1,77 +1,412 @@ | ||||
| <div class="container-md card w-100" style="padding: 1em; background: var(--box-bg)" id=acceleratePreviewAnchor> | ||||
| <div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor> | ||||
| 
 | ||||
|   <div class="row mt-2" *ngIf="showSuccess"> | ||||
|     <div class="col"> | ||||
|       <div class="alert alert-success"> | ||||
|         Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration. | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   @if (error) { | ||||
|     <div class="mt-2"> | ||||
|       <app-mempool-error [error]="error"></app-mempool-error> | ||||
|     </div> | ||||
|   }  | ||||
| 
 | ||||
|   @else if (step === 'cta') { | ||||
|     <!-- Show A/B CTAs --> | ||||
|     <div class="row mb-1"> | ||||
|       <div class="col-sm"> | ||||
|         <h1 style="font-size: larger;">Accelerate your Bitcoin transaction?</h1> | ||||
|     <div class="row mt-2"> | ||||
|       <div class="col"> | ||||
|         <app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error> | ||||
|       </div> | ||||
|     </div> | ||||
|   } | ||||
|   @else if (step === 'quote') { | ||||
|     <div class="accelerate-cols"> | ||||
|       <ng-container *ngIf="!isMobile"> | ||||
|         <app-accelerate-fee-graph | ||||
|           [tx]="tx" | ||||
|           [estimate]="estimate" | ||||
|           [showEstimate]="isLoggedIn()" | ||||
|           [maxRateOptions]="maxRateOptions" | ||||
|           [maxRateIndex]="selectFeeRateIndex" | ||||
|           (setUserBid)="setUserBid($event)" | ||||
|         ></app-accelerate-fee-graph> | ||||
|       </ng-container> | ||||
| 
 | ||||
|     <form> | ||||
|       <div class="row"> | ||||
|         <div class="col-sm"> | ||||
|           <div class="form-group form-check mb-2"> | ||||
|             <input type="radio" class="form-check-input" id="accelerate" name="accelerate" (change)="selectedOptionChanged($event)"> | ||||
|             <label class="form-check-label d-flex flex-column" for="accelerate"> | ||||
|               <span class="font-weight-bold">Accelerate</span> | ||||
|               <span style="color: rgb(186, 186, 186); font-size: 14px;">Confirmation expected within ~30 minutes<br> | ||||
|                 @if (!calculating) { | ||||
|                   <app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span>) | ||||
|                 } @else { | ||||
|                   <span class="estimating">Calculating cost...</span> | ||||
|                 } | ||||
|               </span> | ||||
|             </label> | ||||
|       <ng-container *ngIf="estimate else loadingEstimate"> | ||||
|         <div [class.disabled]="error || showSuccess"> | ||||
|           <div *ngIf="isLoggedIn() && !estimate.hasAccess" style="margin-right: 5em;"> | ||||
|             <div class="alert alert-mempool mr-6">You are currently on the waitlist</div> | ||||
|           </div> | ||||
| 
 | ||||
|           @if (showDetails) { | ||||
|             <h5 i18n="accelerator.your-transaction">Your transaction</h5> | ||||
|             <div class="row"> | ||||
|               <div class="col"> | ||||
|                 <small *ngIf="hasAncestors" class="form-text text-muted mb-2"> | ||||
|                   <ng-container i18n="accelerator.plus-unconfirmed-ancestors">Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s)</ng-container> | ||||
|                 </small> | ||||
|                 <table class="table table-borderless table-border table-dark table-background table-accelerator"> | ||||
|                   <tbody> | ||||
|                     <tr class="group-first"> | ||||
|                       <td class="item" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td> | ||||
|                       <td style="text-align: end;" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td> | ||||
|                     </tr> | ||||
|                     <tr class="info"> | ||||
|                       <td class="info" colspan=3> | ||||
|                         <i><small i18n="accelerator.transaction-vbytes-size-description">Size in vbytes of this transaction (including unconfirmed ancestors)</small></i> | ||||
|                       </td> | ||||
|                     </tr> | ||||
|                     <tr> | ||||
|                       <td class="item" i18n="accelerator.in-band-fees">In-band fees</td> | ||||
|                       <td style="text-align: end;"> | ||||
|                         {{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                       </td> | ||||
|                     </tr> | ||||
|                     <tr class="info group-last"> | ||||
|                       <td class="info" colspan=3> | ||||
|                         <i><small i18n="accelerator.fees-already-paid-description">Fees already paid by this transaction (including unconfirmed ancestors)</small></i> | ||||
|                       </td> | ||||
|                     </tr> | ||||
|                   </tbody> | ||||
|                 </table> | ||||
|               </div> | ||||
|             </div> | ||||
|             <br> | ||||
|           } | ||||
|           <h5 *ngIf="estimate?.pools?.length" i18n="accelerator.how-much-faster">How much faster?</h5> | ||||
|           <div class="row"> | ||||
|             <div class="col"> | ||||
|               <ng-container *ngIf="(etaInfo$ | async) as etaInfo; else loadingEstimate"> | ||||
|                 <small class="form-text checkout-text mb-2" i18n="accelerator.hashrate-percentage-description">Your transaction will be prioritized by up to <strong>{{ etaInfo.hashratePercentage | number : '1.1-1' }}%</strong> of miners.</small> | ||||
|                 <small class="form-text checkout-text mb-2" i18n="accelerator.time-estimate-description">This will reduce your expected waiting time until the first confirmation to <strong><app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time></strong></small> | ||||
|               </ng-container> | ||||
|             </div> | ||||
|             <div class="col pie"> | ||||
|               <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="row"> | ||||
|             <div class="col"> | ||||
|               <div class="form-group"> | ||||
|                 <div class="fee-card"> | ||||
|                   <div class="d-flex mb-0"> | ||||
|                     <ng-container *ngFor="let option of maxRateOptions"> | ||||
|                       <button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)"> | ||||
|                         <span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | number }} <span class="symbol" i18n="shared.sats">sats</span></span> | ||||
|                         <span class="rate">~<app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span> | ||||
|                       </button> | ||||
|                     </ng-container> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <h5>Summary</h5> | ||||
|           <div class="row"> | ||||
|             <div class="col"> | ||||
|               <table class="table table-borderless table-border table-dark table-background table-accelerator"> | ||||
|                 <tbody> | ||||
|                   <!-- ESTIMATED FEE --> | ||||
|                   <ng-container *ngIf="showDetails"> | ||||
|                     @if (isLoggedIn()) { | ||||
|                       <tr class="group-first"> | ||||
|                         <td class="item" i18n="accelerator.next-block-rate">Next block market rate</td> | ||||
|                         <td class="amt" style="font-size: 16px"> | ||||
|                           {{ estimate.targetFeeRate | number : '1.0-0' }} | ||||
|                         </td> | ||||
|                         <td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td> | ||||
|                       </tr> | ||||
|                       <tr class="info"> | ||||
|                         <td class="info"> | ||||
|                           <i><small i18n="accelerator.estimated-extra-fee-required">Estimated extra fee required</small></i> | ||||
|                         </td> | ||||
|                         <td class="amt"> | ||||
|                           {{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }} | ||||
|                         </td> | ||||
|                         <td class="units"> | ||||
|                           <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                           <span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span> | ||||
|                         </td> | ||||
|                       </tr> | ||||
|                     } | ||||
|                     @else { | ||||
|                       <!-- TARGET FEE --> | ||||
|                       <tr class="group-first"> | ||||
|                         <td class="item" i18n="accelerator.target-rate">Target rate</td> | ||||
|                         <td class="amt" style="font-size: 16px"> | ||||
|                           {{ maxRateOptions[selectFeeRateIndex].rate | number : '1.0-0' }} | ||||
|                         </td> | ||||
|                         <td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td> | ||||
|                       </tr> | ||||
|                       <tr class="info"> | ||||
|                         <td class="info"> | ||||
|                           <i><small i18n="accelerator.extra-fee-required">Extra fee required</small></i> | ||||
|                         </td> | ||||
|                         <td class="amt"> | ||||
|                           {{ maxRateOptions[selectFeeRateIndex].fee | number }} | ||||
|                         </td> | ||||
|                         <td class="units"> | ||||
|                           <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                           <span class="fiat ml-1"><app-fiat [value]="maxRateOptions[selectFeeRateIndex].fee"></app-fiat></span> | ||||
|                         </td> | ||||
|                       </tr> | ||||
|                     } | ||||
| 
 | ||||
|                     <!-- MEMPOOL BASE FEE --> | ||||
|                     <tr> | ||||
|                       <td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator™ fees</td> | ||||
|                     </tr> | ||||
|                     <tr class="info" [class.group-last]="!estimate.vsizeFee" [class.dashed-bottom]="!estimate.vsizeFee"> | ||||
|                       <td class="info"> | ||||
|                         <i><small i18n="accelerator.service-fee">Accelerator Service Fee</small></i> | ||||
|                       </td> | ||||
|                       <td class="amt"> | ||||
|                         +{{ estimate.mempoolBaseFee | number }} | ||||
|                       </td> | ||||
|                       <td class="units"> | ||||
|                         <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                         <span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span> | ||||
|                       </td> | ||||
|                     </tr> | ||||
|                     <tr class="info group-last dashed-bottom" *ngIf="estimate.vsizeFee"> | ||||
|                       <td class="info"> | ||||
|                         <i><small i18n="accelerator.tx-size-surcharge">Transaction Size Surcharge</small></i> | ||||
|                       </td> | ||||
|                       <td class="amt"> | ||||
|                         +{{ estimate.vsizeFee | number }} | ||||
|                       </td> | ||||
|                       <td class="units"> | ||||
|                         <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                         <span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span> | ||||
|                       </td> | ||||
|                     </tr> | ||||
|                   </ng-container> | ||||
| 
 | ||||
|                   <!-- NEXT BLOCK ESTIMATE --> | ||||
|                   <ng-container *ngIf="isLoggedIn()"> | ||||
|                     <tr class="group-first"> | ||||
|                       <td class="item"> | ||||
|                         <b style="background-color: #5E35B1" class="p-1 pl-0" i18n="accelerator.estimated-cost">Estimated acceleration cost</b> ~{{ estimate.targetFeeRate | number : '1.0-0' }} sat/vB | ||||
|                       </td> | ||||
|                       <td class="amt"> | ||||
|                         <span style="background-color: #5E35B1" class="p-1 pl-0"> | ||||
|                           {{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }} | ||||
|                         </span> | ||||
|                       </td> | ||||
|                       <td class="units"> | ||||
|                         <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                         <span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span> | ||||
|                       </td> | ||||
|                     </tr> | ||||
|                   </ng-container> | ||||
| 
 | ||||
|                   <!-- MAX COST --> | ||||
|                   <ng-container> | ||||
|                     <tr class="group-first group-last"> | ||||
|                       <td class="item"> | ||||
|                         @if (isLoggedIn()) { | ||||
|                           <b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.maximum-cost">Maximum acceleration cost</b> | ||||
|                         } @else { | ||||
|                           <b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.cost">Acceleration cost</b> | ||||
|                         } | ||||
|                       </td> | ||||
|                       <td class="amt"> | ||||
|                         <span style="background-color: var(--primary)" class="p-1 pl-0"> | ||||
|                           {{ cost | number }} | ||||
|                         </span> | ||||
|                       </td> | ||||
|                       <td class="units"> | ||||
|                         <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                         <span class="fiat ml-1"> | ||||
|                           <app-fiat [value]="cost" [colorClass]="isLoggedIn() && estimate.userBalance < cost ? 'red-color' : 'green-color'"></app-fiat> | ||||
|                         </span> | ||||
|                       </td> | ||||
|                     </tr> | ||||
|                   </ng-container> | ||||
| 
 | ||||
|                   <!-- USER BALANCE --> | ||||
|                   <ng-container *ngIf="isLoggedIn() && estimate.userBalance < cost"> | ||||
|                     <tr class="group-first group-last dashed-top"> | ||||
|                       <td class="item" i18n="accelerator.available-balance">Available balance</td> | ||||
|                       <td class="amt"> | ||||
|                         {{ estimate.userBalance | number }} | ||||
|                       </td> | ||||
|                       <td class="units"> | ||||
|                         <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                         <span class="fiat ml-1"> | ||||
|                           <app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < cost ? 'red-color' : 'green-color'"></app-fiat> | ||||
|                         </span> | ||||
|                       </td> | ||||
|                     </tr> | ||||
|                   </ng-container> | ||||
| 
 | ||||
|                   <tr class="group-first group-last" style="border-top: 1px dashed grey"> | ||||
|                     <td class="item"></td> | ||||
|                     <td colspan="2"> | ||||
|                       <div class="d-flex"> | ||||
|                         <ng-container *ngTemplateOutlet="accelerateButton"></ng-container> | ||||
|                       </div> | ||||
|                     </td> | ||||
|                   </tr> | ||||
|                 </tbody> | ||||
|               </table> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </ng-container> | ||||
|     </div> | ||||
| 
 | ||||
|     <hr> | ||||
|     <div class="row mt-2 mb-2 text-center"> | ||||
|       <div class="col-sm d-flex flex-column"> | ||||
|         <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')">Go Back</button> | ||||
|       </div> | ||||
|       <div class="row"> | ||||
|         <div class="col-sm"> | ||||
|           <div class="form-group form-check mb-2"> | ||||
|             <input type="radio" class="form-check-input" id="wait" name="accelerate" (change)="selectedOptionChanged($event)"> | ||||
|             <label class="form-check-label d-flex flex-column" for="wait"> | ||||
|               <span class="font-weight-bold">Wait</span> | ||||
|               @if (eta) { | ||||
|                 <span style="color: rgb(186, 186, 186); font-size: 14px;">Confirmation expected <app-time kind="within" [time]="eta" [fastRender]="false" [fixedRender]="true"></app-time></span> | ||||
|               } @else { | ||||
|                 <span style="color: rgb(186, 186, 186); font-size: 14px;"> | ||||
|                   <span>Settlement expected within several hours</span> | ||||
|     </div> | ||||
| 
 | ||||
|     <ng-template #loadingEstimate> | ||||
|       <div class="skeleton-loader"></div> | ||||
|       <br> | ||||
|     </ng-template> | ||||
|   } | ||||
|   @else if (step === 'summary') { | ||||
|     <ng-container *ngIf="estimate && (etaInfo$ | async) as etaInfo; else loadingSummary"> | ||||
|       <!-- Show A/B CTAs --> | ||||
|       @if (!noCTA) { | ||||
|         <div class="row mb-1"> | ||||
|           <div class="col-sm"> | ||||
|             <h1 style="font-size: larger;"><ng-content select="[slot='cta-title']"></ng-content><span class="default-slot">Accelerate your Bitcoin transaction?</span></h1> | ||||
|           </div> | ||||
|         </div> | ||||
|       } | ||||
| 
 | ||||
|       <div *ngIf="isLoggedIn() && !estimate.hasAccess"> | ||||
|         <div class="alert alert-mempool mr-6">You are currently on the waitlist for Mempool Accelerator™</div> | ||||
|       </div> | ||||
| 
 | ||||
|       <form [class.disabled]="error || showSuccess"> | ||||
|         <div class="row summary-row"> | ||||
|           <div> | ||||
|             <div class="form-group form-check mb-2"> | ||||
|               <div class="float-right"><ng-container *ngTemplateOutlet="customizeButton"></ng-container></div> | ||||
|               <input type="checkbox" [checked]="armed" class="form-check-input" [class.error-shake]="misfire" id="accel" name="accel" (change)="armed = !armed; misfire = false"> | ||||
|               <label class="form-check-label d-flex flex-column" for="accel"> | ||||
|                 <span><b>Accelerate</b> to ~{{ ((userBid + estimate.txSummary.effectiveFee) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} sat/vB</span> | ||||
|                 <span class="checkout-text">Confirmation expected <app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time><br> | ||||
|                   @if (!calculating) { | ||||
|                     <app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span>) | ||||
|                   } @else { | ||||
|                     <span class="estimating">Calculating cost...</span> | ||||
|                   } | ||||
|                 </span> | ||||
|               } | ||||
|             </label> | ||||
|               </label> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="pie d-none d-lg-flex" *ngIf="!forceMobile"> | ||||
|             <small class="form-text checkout-text mb-2" i18n="accelerator.hashrate-percentage-description">Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners.</small> | ||||
|             <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box> | ||||
|           </div> | ||||
|           <ng-container *ngTemplateOutlet="accelerateButton"></ng-container> | ||||
|         </div> | ||||
|       </form> | ||||
|     </ng-container> | ||||
|     <ng-template #loadingSummary> | ||||
|       <div class="row"> | ||||
|         <div class="col-md"> | ||||
|           <div class="d-flex flex-row justify-content-center align-items-center"> | ||||
|             <div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="row mt-2 mb-2" [style]="(choosenOption === 'wait' || calculating) ? 'opacity: 0.25; pointer-events: none' : ''"> | ||||
|         <div class="col-sm d-flex flex-row justify-content-center"> | ||||
|           <button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" style="width: 200px" (click)="enableCheckoutPage()"> | ||||
|     </ng-template> | ||||
|   } @else if (step === 'checkout') { | ||||
|     <ng-container *ngIf="estimate && (etaInfo$ | async) as etaInfo; else loadingCheckout"> | ||||
|       <div class="row"> | ||||
|         <div class="col-md"> | ||||
|           <div class="d-flex flex-column"> | ||||
|             <span><b>Accelerate</b> to ~{{ ((userBid + estimate.txSummary.effectiveFee) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} sat/vB</span> | ||||
|             <span class="checkout-text"> | ||||
|               @if (!calculating) { | ||||
|                 For an additional <app-fiat [value]="cost"></app-fiat> (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span>) | ||||
|               } @else { | ||||
|                 <span class="estimating">Calculating cost...</span> | ||||
|               } | ||||
|             </span> | ||||
|             <span class="checkout-text" *ngIf="(etaInfo$ | async) as etaInfo"> | ||||
|               Reducing expected confirmation time to <app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time> | ||||
|             </span> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="col-md pie d-none d-md-flex" *ngIf="!forceMobile"> | ||||
|           <small class="form-text checkout-text mb-2" i18n="accelerator.hashrate-percentage-description" *ngIf="(etaInfo$ | async) as etaInfo">Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners.</small> | ||||
|           <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box> | ||||
|         </div> | ||||
|       </div> | ||||
|       @if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) { | ||||
|         <div class="d-flex justify-content-center" [class.grayOut]="!canPayWithBalance || error || showSuccess"> | ||||
|           <button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" style="width: 200px" (click)="accelerate()"> | ||||
|             <img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px"> | ||||
|             <span>Accelerate</span> | ||||
|           </button> | ||||
|         </div> | ||||
|       } @else { | ||||
|         <div class="payment-area mt-2 p-2" [class.disabled]="error || showSuccess"> | ||||
|           <div class="row text-center justify-content-center mx-2" style="font-size: 14px;"> | ||||
|             <p>Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank"> {{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p> | ||||
|           </div> | ||||
|           <div class="row"> | ||||
|             @if (canPayWithBitcoin) { | ||||
|               <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> | ||||
|                 @if (invoice) { | ||||
|                   <p>Pay <span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span></p> | ||||
|                   <app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [invoiceId]="invoice.id" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice> | ||||
|                 } @else { | ||||
|                   <p>Loading invoice...</p> | ||||
|                   <div class="d-flex align-items-center justify-content-center" style="width: 100%; height: 292px;"> | ||||
|                     <div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div> | ||||
|                   </div> | ||||
|                 } | ||||
|               </div> | ||||
|               @if (canPayWithCashapp) { | ||||
|                 <div class="col-sm text-center flex-grow-0  d-flex flex-column justify-content-center align-items-center"> | ||||
|                   <p class="text-nowrap">—<span i18n="or">OR</span>—</p> | ||||
|                 </div> | ||||
|               } | ||||
|             } | ||||
|             @if (canPayWithCashapp) { | ||||
|               <div class="col-sm text-center d-flex flex-column justify-content-center align-items-center"> | ||||
|                 <p>Pay <app-fiat [value]="cost"></app-fiat> with</p> | ||||
|                 <img class="paymentMethod mx-2" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')"> | ||||
|               </div> | ||||
|             } | ||||
|           </div> | ||||
|         </div> | ||||
|       } | ||||
|     </ng-container> | ||||
|     <ng-template #loadingCheckout> | ||||
|       <div class="row"> | ||||
|         <div class="col-md"> | ||||
|           <div class="d-flex flex-row justify-content-center align-items-center"> | ||||
|             <div class="m-4 spinner-border text-light" style="width: 25px; height: 25px"></div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </form> | ||||
|   } | ||||
|      | ||||
|   @else if (step === 'checkout') { | ||||
|     </ng-template> | ||||
| 
 | ||||
|     <hr> | ||||
|     <div class="row mt-2 mb-2 text-center"> | ||||
|       <div class="col-sm d-flex flex-column"> | ||||
|         <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')">Go Back</button> | ||||
|       </div> | ||||
|     </div> | ||||
|   } @else if (step === 'cashapp') { | ||||
|     <!-- Show checkout page --> | ||||
|     <div class="row mb-md-1 text-center"> | ||||
|       <div class="col-sm"> | ||||
|         <h1 style="font-size: larger;">Confirm your payment</h1> | ||||
|       <div class="col-sm" id="confirm-payment-title"> | ||||
|         <h1 style="font-size: larger;"><ng-content select="[slot='checkout-title']"></ng-content><span class="default-slot">Confirm your payment</span></h1> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="row text-center"> | ||||
|       <div class="col-sm"> | ||||
|         <div class="form-group w-100" style="font-size: 14px"> | ||||
|           Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + txid" target="_blank">{{ txid.substr(0, 10) }}..{{ txid.substr(-10) }}</a> | ||||
|           Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| @ -109,16 +444,14 @@ | ||||
|     <hr> | ||||
|     <div class="row mt-2 mb-2 text-center"> | ||||
|       <div class="col-sm d-flex flex-column"> | ||||
|         <small>Changed your mind?</small> | ||||
|         <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="step = 'cta'">Go Back</button> | ||||
|         <button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('checkout')">Go Back</button> | ||||
|       </div> | ||||
|     </div> | ||||
|   } | ||||
| 
 | ||||
|   @else if (step === 'processing') { | ||||
|     <div class="row mb-1 text-center"> | ||||
|       <div class="col-sm"> | ||||
|         <h1 style="font-size: larger;">Confirm your payment</h1> | ||||
|         <h1 style="font-size: larger;"><ng-content select="[slot='processing-title']"></ng-content><span class="default-slot">Confirming your payment</span></h1> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
| @ -135,5 +468,38 @@ | ||||
|       </div> | ||||
|     </div> | ||||
|   } | ||||
|    | ||||
|   @else if (step === 'paid') { | ||||
|     <div class="row mb-1 text-center"> | ||||
|       <div class="col-sm"> | ||||
|         <h1 style="font-size: larger;"><ng-content select="[slot='accelerating-title']"></ng-content><span class="default-slot">Accelerating your transaction</span></h1> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="row text-center mt-1"> | ||||
|       <div class="col-sm"> | ||||
|         <div class="d-flex flex-row justify-content-center align-items-center"> | ||||
|           <span>Confirming your acceleration with our mining pool partners...</span> | ||||
|           <div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   } | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #customizeButton> | ||||
|   <button type="button" *ngIf="advancedEnabled" class="btn btn-sm btn-outline-info btn-small-height ml-3" (click)="moveToStep('quote')" i18n="accelerator.customize">customize</button> | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #accelerateButton> | ||||
|   @if (isLoggedIn() || canPayWithBitcoin || canPayWithCashapp) { | ||||
|     <button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" [class.grayOut]="!canPay || (!armed && step === 'summary') || calculating" style="width: 200px" (click)="accelerate()"> | ||||
|       <img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px"> | ||||
|       <span>Accelerate</span> | ||||
|     </button> | ||||
|   } @else { | ||||
|     <button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center disabled" style="width: 200px"> | ||||
|       <img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px"> | ||||
|       <span>Coming soon</span> | ||||
|     </button> | ||||
|   } | ||||
| </ng-template> | ||||
| @ -7,3 +7,204 @@ | ||||
| .estimating { | ||||
|   color: var(--green) | ||||
| } | ||||
| 
 | ||||
| .paymentMethod { | ||||
|   padding: 10px; | ||||
|   background-color: var(--secondary); | ||||
|   border-radius: 15px; | ||||
|   border: 2px solid var(--bg); | ||||
|   cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .default-slot:not(:only-child) { | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| .pie { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   max-width: 330px; | ||||
| } | ||||
| 
 | ||||
| .fee-card { | ||||
|   padding: 15px; | ||||
|   background-color: var(--bg); | ||||
| 
 | ||||
|   .feerate { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| 
 | ||||
|     .rate { | ||||
|       font-size: 0.9em; | ||||
|       .symbol { | ||||
|         color: white; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .btn-border { | ||||
|   border: solid 1px black; | ||||
|   background-color: #0c4a87; | ||||
| } | ||||
| 
 | ||||
| .feerate.active { | ||||
|   background-color: var(--primary) !important; | ||||
|   opacity: 1; | ||||
|   border: 1px solid #007fff !important; | ||||
| } | ||||
| .feerate:focus { | ||||
|   box-shadow: none !important; | ||||
| } | ||||
| 
 | ||||
| .grayOut { | ||||
|   opacity: 0.5; | ||||
| } | ||||
| 
 | ||||
| .disabled { | ||||
|   opacity: 0.5; | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| .table-toggle { | ||||
|   width: 100%; | ||||
|   margin-top: 0.5em; | ||||
| } | ||||
| 
 | ||||
| .tab { | ||||
|   &:first-child { | ||||
|     margin-right: 1px; | ||||
|   } | ||||
|   border: solid 1px black; | ||||
|   border-bottom: none; | ||||
|   background-color: #323655; | ||||
|   border-top-left-radius: 10px !important; | ||||
|   border-top-right-radius: 10px !important; | ||||
| } | ||||
| .tab.active { | ||||
|   background-color: #5d659d !important; | ||||
|   opacity: 1; | ||||
| } | ||||
| .tab:focus { | ||||
|   box-shadow: none !important; | ||||
| } | ||||
| 
 | ||||
| .table-accelerator { | ||||
|   tr { | ||||
|     td { | ||||
|       padding-top: 0; | ||||
|       padding-bottom: 0; | ||||
|       vertical-align: baseline; | ||||
|     } | ||||
| 
 | ||||
|     &.group-first { | ||||
|       td { | ||||
|         padding-top: 0.75rem; | ||||
|       } | ||||
|     } | ||||
|     &.group-last, &:last-child { | ||||
|       td { | ||||
|         padding-bottom: 0.75rem; | ||||
|       } | ||||
|     } | ||||
|     &.dashed-top { | ||||
|       border-top: 1px dashed grey; | ||||
|     } | ||||
|     &.dashed-bottom { | ||||
|       border-bottom: 1px dashed grey | ||||
|     } | ||||
|   } | ||||
|   td { | ||||
|     &:first-child { | ||||
|       width: 100vw; | ||||
|     } | ||||
|     &.info { | ||||
|       color: #6c757d; | ||||
|       white-space: initial; | ||||
|     } | ||||
|     &.amt { | ||||
|       text-align: right; | ||||
|       padding-right: 0.2em; | ||||
|     } | ||||
|     &.units { | ||||
|       padding-left: 0.2em; | ||||
|       white-space: nowrap; | ||||
|       display: flex; | ||||
|       justify-content: space-between; | ||||
|       align-items: center; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .accelerate-cols { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   align-items: stretch; | ||||
|   margin-top: 1em; | ||||
| } | ||||
| 
 | ||||
| .payment-area { | ||||
|   background: var(--bg); | ||||
| } | ||||
| 
 | ||||
| .col.pie { | ||||
|   flex-grow: 0; | ||||
|   padding: 0 1em; | ||||
|   position: relative; | ||||
|   top: -15px; | ||||
| } | ||||
| 
 | ||||
| .item { | ||||
|   white-space: initial; | ||||
| } | ||||
| 
 | ||||
| .table-background { | ||||
|   background-color: var(--bg); | ||||
| } | ||||
| 
 | ||||
| .checkout-text { | ||||
|   color: rgb(186, 186, 186); | ||||
|   font-size: 14px; | ||||
| } | ||||
| 
 | ||||
| .btn-accelerate { | ||||
|   background-color: var(--tertiary); | ||||
| } | ||||
| 
 | ||||
| .btn-small-height { | ||||
| 	line-height: 1; | ||||
| } | ||||
| 
 | ||||
| .summary-row { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   padding: 0 2em; | ||||
|   flex-wrap: wrap; | ||||
| 
 | ||||
|   @media (max-width: 640px) { | ||||
|     flex-direction: column; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes box-shake { | ||||
|   0% { transform: rotate(0deg); } | ||||
|   10% { transform: rotate(-8deg); } | ||||
|   20% { transform: rotate(8deg); } | ||||
|   30% { transform: rotate(-8deg); } | ||||
|   40% { transform: rotate(8deg); } | ||||
|   50% { transform: rotate(-8deg); } | ||||
|   60% { transform: rotate(8deg); } | ||||
|   70% { transform: rotate(-8deg); } | ||||
|   80% { transform: rotate(8deg); } | ||||
|   90% { transform: rotate(-8deg); } | ||||
|   100% { transform: rotate(0deg); } | ||||
| } | ||||
| 
 | ||||
| .error-shake { | ||||
|   box-shadow: 0 0 10px 2px var(--danger); | ||||
|   animation: box-shake 1.5s ease-in-out; | ||||
| } | ||||
| @ -1,9 +1,47 @@ | ||||
| import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges } from '@angular/core'; | ||||
| import { Subscription, tap, of, catchError } from 'rxjs'; | ||||
| import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core'; | ||||
| import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs'; | ||||
| import { ServicesApiServices } from '../../services/services-api.service'; | ||||
| import { nextRoundNumber } from '../../shared/common.utils'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { AudioService } from '../../services/audio.service'; | ||||
| import { ETA, EtaService } from '../../services/eta.service'; | ||||
| import { Transaction } from '../../interfaces/electrs.interface'; | ||||
| import { MiningStats } from '../../services/mining.service'; | ||||
| import { StorageService } from '../../services/storage.service'; | ||||
| 
 | ||||
| export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp'; | ||||
| 
 | ||||
| export type AccelerationEstimate = { | ||||
|   hasAccess: boolean; | ||||
|   txSummary: TxSummary; | ||||
|   nextBlockFee: number; | ||||
|   targetFeeRate: number; | ||||
|   userBalance: number; | ||||
|   enoughBalance: boolean; | ||||
|   cost: number; | ||||
|   mempoolBaseFee: number; | ||||
|   vsizeFee: number; | ||||
|   pools: number[]; | ||||
|   availablePaymentMethods: PaymentMethod[]; | ||||
| } | ||||
| export type TxSummary = { | ||||
|   txid: string; // txid of the current transaction
 | ||||
|   effectiveVsize: number; // Total vsize of the dependency tree
 | ||||
|   effectiveFee: number;  // Total fee of the dependency tree in sats
 | ||||
|   ancestorCount: number; // Number of ancestors
 | ||||
| } | ||||
| 
 | ||||
| export interface RateOption { | ||||
|   fee: number; | ||||
|   rate: number; | ||||
|   index: number; | ||||
| } | ||||
| 
 | ||||
| export const MIN_BID_RATIO = 1; | ||||
| export const DEFAULT_BID_RATIO = 2; | ||||
| export const MAX_BID_RATIO = 4; | ||||
| 
 | ||||
| type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'processing' | 'paid'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-accelerate-checkout', | ||||
| @ -11,21 +49,50 @@ import { AudioService } from '../../services/audio.service'; | ||||
|   styleUrls: ['./accelerate-checkout.component.scss'] | ||||
| }) | ||||
| export class AccelerateCheckout implements OnInit, OnDestroy { | ||||
|   @Input() eta: number | null = null; | ||||
|   @Input() txid: string = '70c18d76cdb285a1b5bd87fdaae165880afa189809c30b4083ff7c0e69ee09ad'; | ||||
|   @Input() tx: Transaction; | ||||
|   @Input() miningStats: MiningStats; | ||||
|   @Input() eta: ETA; | ||||
|   @Input() scrollEvent: boolean; | ||||
|   @Output() close = new EventEmitter<null>(); | ||||
|   @Input() cashappEnabled: boolean = true; | ||||
|   @Input() advancedEnabled: boolean = false; | ||||
|   @Input() forceMobile: boolean = false; | ||||
|   @Input() showDetails: boolean = false; | ||||
|   @Input() noCTA: boolean = false; | ||||
|   @Output() hasDetails = new EventEmitter<boolean>(); | ||||
|   @Output() changeMode = new EventEmitter<boolean>(); | ||||
| 
 | ||||
|   calculating = true; | ||||
|   choosenOption: 'wait' | 'accelerate' = 'wait'; | ||||
|   armed = false; | ||||
|   misfire = false; | ||||
|   error = ''; | ||||
|   math = Math; | ||||
|   isMobile: boolean = window.innerWidth <= 767.98; | ||||
| 
 | ||||
|   private _step: CheckoutStep = 'summary'; | ||||
|   simpleMode: boolean = true; | ||||
|   paymentMethod: 'cashapp' | 'btcpay'; | ||||
| 
 | ||||
|   user: any = undefined; | ||||
| 
 | ||||
|   // accelerator stuff
 | ||||
|   square: { appId: string, locationId: string}; | ||||
|   accelerationUUID: string; | ||||
|   accelerationSubscription: Subscription; | ||||
|   difficultySubscription: Subscription; | ||||
|   estimateSubscription: Subscription; | ||||
|   estimate: AccelerationEstimate; | ||||
|   maxBidBoost: number; // sats
 | ||||
|   cost: number; // sats
 | ||||
|   etaInfo$: Observable<{ hashratePercentage: number, ETA: number, acceleratedETA: number }>; | ||||
|   showSuccess = false; | ||||
|   hasAncestors: boolean = false; | ||||
|   minExtraCost = 0; | ||||
|   minBidAllowed = 0; | ||||
|   maxBidAllowed = 0; | ||||
|   defaultBid = 0; | ||||
|   userBid = 0; | ||||
|   selectFeeRateIndex = 1; | ||||
|   maxRateOptions: RateOption[] = []; | ||||
| 
 | ||||
|   // square
 | ||||
|   loadingCashapp = false; | ||||
| @ -34,11 +101,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | ||||
|   cashAppPay: any; | ||||
|   cashAppSubscription: Subscription; | ||||
|   conversionsSubscription: Subscription; | ||||
|   step: 'cta' | 'checkout' | 'processing' = 'cta'; | ||||
|    | ||||
|   // btcpay
 | ||||
|   loadingBtcpayInvoice = false; | ||||
|   invoice = undefined; | ||||
| 
 | ||||
|   constructor( | ||||
|     public stateService: StateService, | ||||
|     private servicesApiService: ServicesApiServices, | ||||
|     private stateService: StateService, | ||||
|     private storageService: StorageService, | ||||
|     private etaService: EtaService, | ||||
|     private audioService: AudioService, | ||||
|     private cd: ChangeDetectorRef | ||||
|   ) { | ||||
| @ -46,11 +118,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.user = this.storageService.getAuth()?.user ?? null; | ||||
|     const urlParams = new URLSearchParams(window.location.search); | ||||
|     if (urlParams.get('cash_request_id')) { // Redirected from cashapp
 | ||||
|       this.moveToStep('processing'); | ||||
|       this.insertSquare(); | ||||
|       this.setupSquare(); | ||||
|       this.step = 'processing'; | ||||
|     } else { | ||||
|       this.moveToStep('summary'); | ||||
|     } | ||||
| 
 | ||||
|     this.servicesApiService.setupSquare$().subscribe(ids => { | ||||
| @ -58,9 +133,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | ||||
|         appId: ids.squareAppId, | ||||
|         locationId: ids.squareLocationId | ||||
|       }; | ||||
|       if (this.step === 'cta') { | ||||
|         this.estimate(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| @ -71,20 +143,38 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     if (changes.scrollEvent) { | ||||
|       this.scrollToPreview('acceleratePreviewAnchor', 'start'); | ||||
|     if (changes.scrollEvent && this.scrollEvent) { | ||||
|       this.scrollToElement('acceleratePreviewAnchor', 'start'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   moveToStep(step: CheckoutStep) { | ||||
|     this._step = step; | ||||
|     this.misfire = false; | ||||
|     if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) { | ||||
|       this.fetchEstimate(); | ||||
|     } | ||||
|     if (this._step === 'checkout' && this.canPayWithBitcoin) { | ||||
|       this.loadingBtcpayInvoice = true; | ||||
|       this.invoice = null; | ||||
|       this.requestBTCPayInvoice(); | ||||
|     } else if (this._step === 'cashapp' && this.cashappEnabled) { | ||||
|       this.loadingCashapp = true; | ||||
|       this.insertSquare(); | ||||
|       this.setupSquare(); | ||||
|     } | ||||
|     this.hasDetails.emit(this._step === 'quote'); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|   * Scroll to element id with or without setTimeout | ||||
|   */ | ||||
|   scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) { | ||||
|   scrollToElementWithTimeout(id: string, position: ScrollLogicalPosition, timeout: number = 1000): void { | ||||
|     setTimeout(() => { | ||||
|       this.scrollToPreview(id, position); | ||||
|     }, 1000); | ||||
|       this.scrollToElement(id, position); | ||||
|     }, timeout); | ||||
|   } | ||||
|   scrollToPreview(id: string, position: ScrollLogicalPosition) { | ||||
|   scrollToElement(id: string, position: ScrollLogicalPosition) { | ||||
|     const acceleratePreviewAnchor = document.getElementById(id); | ||||
|     if (acceleratePreviewAnchor) { | ||||
|       this.cd.markForCheck(); | ||||
| @ -99,37 +189,128 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | ||||
|   /** | ||||
|    * Accelerator | ||||
|    */ | ||||
|   estimate() { | ||||
|   fetchEstimate() { | ||||
|     if (this.estimateSubscription) { | ||||
|       this.estimateSubscription.unsubscribe(); | ||||
|     } | ||||
|     this.calculating = true; | ||||
|     this.estimateSubscription = this.servicesApiService.estimate$(this.txid).pipe( | ||||
|     this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe( | ||||
|       tap((response) => { | ||||
|         this.calculating = false; | ||||
|         if (response.status === 204) { | ||||
|           this.error = `cannot_accelerate_tx`; | ||||
|         } else { | ||||
|           const estimation = response.body; | ||||
|           if (!estimation) { | ||||
|           this.estimate = response.body; | ||||
|           if (!this.estimate) { | ||||
|             this.error = `cannot_accelerate_tx`; | ||||
|             return; | ||||
|           } | ||||
|           if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) { | ||||
|             if (this.isLoggedIn()) { | ||||
|               this.error = `not_enough_balance`; | ||||
|             } | ||||
|           } | ||||
|           this.hasAncestors = this.estimate.txSummary.ancestorCount > 1; | ||||
|           this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate, this.miningStats); | ||||
| 
 | ||||
|           // Make min extra fee at least 50% of the current tx fee
 | ||||
|           const minExtraBoost = nextRoundNumber(Math.max(estimation.cost * 2, estimation.txSummary.effectiveFee)); | ||||
|           const DEFAULT_BID_RATIO = 1.5; | ||||
|           this.maxBidBoost = minExtraBoost * DEFAULT_BID_RATIO; | ||||
|           this.cost = this.maxBidBoost + estimation.mempoolBaseFee + estimation.vsizeFee; | ||||
|           this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee)); | ||||
| 
 | ||||
|           this.maxRateOptions = [1, 2, 4].map((multiplier, index) => { | ||||
|             return { | ||||
|               fee: this.minExtraCost * multiplier, | ||||
|               rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize, | ||||
|               index, | ||||
|             }; | ||||
|           }); | ||||
| 
 | ||||
|           this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO; | ||||
|           this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO; | ||||
|           this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO; | ||||
| 
 | ||||
|           this.userBid = this.defaultBid; | ||||
|           if (this.userBid < this.minBidAllowed) { | ||||
|             this.userBid = this.minBidAllowed; | ||||
|           } else if (this.userBid > this.maxBidAllowed) { | ||||
|             this.userBid = this.maxBidAllowed; | ||||
|           } | ||||
|           this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; | ||||
| 
 | ||||
|           if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) { | ||||
|             this.loadingBtcpayInvoice = true; | ||||
|             this.requestBTCPayInvoice(); | ||||
|           } | ||||
| 
 | ||||
|           this.calculating = false; | ||||
|           this.cd.markForCheck(); | ||||
|         } | ||||
|       }), | ||||
| 
 | ||||
|       catchError((response) => { | ||||
|         this.estimate = undefined; | ||||
|         this.error = `cannot_accelerate_tx`; | ||||
|         this.estimateSubscription.unsubscribe(); | ||||
|         return of(null); | ||||
|       }) | ||||
|     ).subscribe(); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * User changed his bid | ||||
|    */ | ||||
|   setUserBid({ fee, index }: { fee: number, index: number}): void { | ||||
|     if (this.estimate) { | ||||
|       this.selectFeeRateIndex = index; | ||||
|       this.userBid = Math.max(0, fee); | ||||
|       this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Advanced mode acceleration button clicked | ||||
|    */ | ||||
|   accelerate(): void { | ||||
|     if (this.canPay && !this.calculating) { | ||||
|       if ((!this.armed && this.step === 'summary')) { | ||||
|         this.misfire = true; | ||||
|       } else { | ||||
|         if (this.isLoggedIn()) { | ||||
|           this.accelerateWithMempoolAccount(); | ||||
|         } else { | ||||
|           this.armed = true; | ||||
|           this.moveToStep('checkout'); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Account-based acceleration request | ||||
|    */ | ||||
|   accelerateWithMempoolAccount(): void { | ||||
|     if (this.accelerationSubscription) { | ||||
|       this.accelerationSubscription.unsubscribe(); | ||||
|     } | ||||
|     this.accelerationSubscription = this.servicesApiService.accelerate$( | ||||
|       this.tx.txid, | ||||
|       this.userBid, | ||||
|       this.accelerationUUID | ||||
|     ).subscribe({ | ||||
|       next: () => { | ||||
|         this.audioService.playSound('ascend-chime-cartoon'); | ||||
|         this.showSuccess = true; | ||||
|         this.estimateSubscription.unsubscribe(); | ||||
|         this.moveToStep('paid') | ||||
|       }, | ||||
|       error: (response) => { | ||||
|         if (response.status === 403 && response.error === 'not_available') { | ||||
|           this.error = 'waitlisted'; | ||||
|         } else { | ||||
|           this.error = response.error; | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Square | ||||
|    */ | ||||
| @ -199,17 +380,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | ||||
|             amount: costUSD.toString(), | ||||
|             label: 'Total', | ||||
|             pending: true, | ||||
|             productUrl: `${redirectHostname}/tracker/${this.txid}`, | ||||
|             productUrl: `${redirectHostname}/tracker/${this.tx.txid}`, | ||||
|           }, | ||||
|           button: { shape: 'semiround', size: 'small', theme: 'light'} | ||||
|         }); | ||||
|         this.cashAppPay = await this.payments.cashAppPay(paymentRequest, { | ||||
|           redirectURL: `${redirectHostname}/tracker/${this.txid}`, | ||||
|           referenceId: `accelerator-${this.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, | ||||
|           redirectURL: `${redirectHostname}/tracker/${this.tx.txid}`, | ||||
|           referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, | ||||
|           button: { shape: 'semiround', size: 'small', theme: 'light'} | ||||
|         }); | ||||
| 
 | ||||
|         if (this.step === 'checkout') { | ||||
|         if (this.step === 'cashapp') { | ||||
|           await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' }) | ||||
|         } | ||||
|         this.loadingCashapp = false; | ||||
| @ -221,7 +402,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | ||||
|             this.error = error; | ||||
|           } else if (tokenResult.status === 'OK') { | ||||
|             that.servicesApiService.accelerateWithCashApp$( | ||||
|               that.txid, | ||||
|               that.tx.txid, | ||||
|               tokenResult.token, | ||||
|               tokenResult.details.cashAppPay.cashtag, | ||||
|               tokenResult.details.cashAppPay.referenceId, | ||||
| @ -233,7 +414,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | ||||
|                   that.cashAppPay.destroy(); | ||||
|                 } | ||||
|                 setTimeout(() => { | ||||
|                   that.closeModal(); | ||||
|                   this.moveToStep('paid'); | ||||
|                   if (window.history.replaceState) { | ||||
|                     const urlParams = new URLSearchParams(window.location.search); | ||||
|                     window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, '')); | ||||
| @ -260,18 +441,56 @@ export class AccelerateCheckout implements OnInit, OnDestroy { | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * UI events | ||||
|    * BTCPay | ||||
|    */ | ||||
|   enableCheckoutPage() { | ||||
|     this.step = 'checkout'; | ||||
|     this.loadingCashapp = true; | ||||
|     this.insertSquare(); | ||||
|     this.setupSquare(); | ||||
|   async requestBTCPayInvoice() { | ||||
|     this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe( | ||||
|       switchMap(response => { | ||||
|         return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId); | ||||
|       }), | ||||
|       catchError(error => { | ||||
|         console.log(error); | ||||
|         return of(null); | ||||
|       }) | ||||
|     ).subscribe((invoice) => { | ||||
|         this.invoice = invoice; | ||||
|         this.cd.markForCheck(); | ||||
|     }); | ||||
|   } | ||||
|   selectedOptionChanged(event) { | ||||
|     this.choosenOption = event.target.id; | ||||
| 
 | ||||
|   bitcoinPaymentCompleted(): void { | ||||
|     this.audioService.playSound('ascend-chime-cartoon'); | ||||
|     this.estimateSubscription.unsubscribe(); | ||||
|     this.moveToStep('paid') | ||||
|   } | ||||
|   closeModal(): void { | ||||
|     this.close.emit(); | ||||
| 
 | ||||
|   isLoggedIn(): boolean { | ||||
|     const auth = this.storageService.getAuth(); | ||||
|     return auth !== null; | ||||
|   } | ||||
| 
 | ||||
|   get step() { | ||||
|     return this._step; | ||||
|   } | ||||
| 
 | ||||
|   get canPayWithBitcoin() { | ||||
|     return this.estimate?.availablePaymentMethods?.includes('bitcoin'); | ||||
|   } | ||||
| 
 | ||||
|   get canPayWithCashapp() { | ||||
|     return this.cashappEnabled && this.estimate?.availablePaymentMethods?.includes('cashapp') && this.cost < 400000 && this.stateService.referrer === 'https://cash.app/'; | ||||
|   } | ||||
| 
 | ||||
|   get canPayWithBalance() { | ||||
|     return this.isLoggedIn() && this.estimate?.availablePaymentMethods?.includes('balance') && this.estimate?.hasAccess; | ||||
|   } | ||||
| 
 | ||||
|   get canPay() { | ||||
|     return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp; | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   onResize(): void { | ||||
|     this.isMobile = window.innerWidth <= 767.98; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -4,7 +4,6 @@ | ||||
|   width: 120px; | ||||
|   margin-left: 4em; | ||||
|   margin-right: 1.5em; | ||||
|   padding-bottom: 63px; | ||||
| 
 | ||||
|   .column { | ||||
|     width: 100%; | ||||
| @ -5,7 +5,7 @@ import { Router } from '@angular/router'; | ||||
| import { ReplaySubject, merge, Subscription, of } from 'rxjs'; | ||||
| import { tap, switchMap } from 'rxjs/operators'; | ||||
| import { ApiService } from '../../services/api.service'; | ||||
| import { AccelerationEstimate, RateOption } from './accelerate-preview.component'; | ||||
| import { AccelerationEstimate, RateOption } from './accelerate-checkout.component'; | ||||
| 
 | ||||
| interface GraphBar { | ||||
|   rate: number; | ||||
| @ -25,6 +25,7 @@ interface GraphBar { | ||||
| export class AccelerateFeeGraphComponent implements OnInit, OnChanges { | ||||
|   @Input() tx: Transaction; | ||||
|   @Input() estimate: AccelerationEstimate; | ||||
|   @Input() showEstimate = false; | ||||
|   @Input() maxRateOptions: RateOption[] = []; | ||||
|   @Input() maxRateIndex: number = 0; | ||||
|   @Output() setUserBid = new EventEmitter<{ fee: number, index: number }>(); | ||||
| @ -52,7 +53,7 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges { | ||||
|         rate: option.rate, | ||||
|         style: this.getStyle(option.rate, maxRate, baseHeight), | ||||
|         class: 'max', | ||||
|         label: $localize`maximum`, | ||||
|         label: this.showEstimate ? $localize`maximum` : $localize`accelerated`, | ||||
|         active: option.index === this.maxRateIndex, | ||||
|         rateIndex: option.index, | ||||
|         fee: option.fee, | ||||
| @ -1,261 +0,0 @@ | ||||
| <span id="successAlert" class="m-0 p-0 d-block" style="height: 1px;"></span> | ||||
| <div class="row" *ngIf="showSuccess"> | ||||
|   <div class="col"> | ||||
|     <div class="alert alert-success"> | ||||
|       Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration. | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <span id="mempoolError" class="m-0 p-0 d-block" style="height: 1px;"></span> | ||||
| <div class="row" *ngIf="error"> | ||||
|   <div class="col"> | ||||
|     <app-mempool-error [error]="error" [alertClass]="error === 'waitlisted' ? 'alert-mempool' : 'alert-danger'"></app-mempool-error> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <div class="accelerate-cols"> | ||||
|   <ng-container *ngIf="!isMobile"> | ||||
|     <app-accelerate-fee-graph | ||||
|       [tx]="tx" | ||||
|       [estimate]="estimate" | ||||
|       [maxRateOptions]="maxRateOptions" | ||||
|       [maxRateIndex]="selectFeeRateIndex" | ||||
|       (setUserBid)="setUserBid($event)" | ||||
|     ></app-accelerate-fee-graph> | ||||
|   </ng-container> | ||||
| 
 | ||||
|   <ng-container *ngIf="estimate else loadingEstimate"> | ||||
|     <div [class]="{estimateDisabled: error || showSuccess }"> | ||||
| 
 | ||||
|       <div *ngIf="user && !estimate.hasAccess"> | ||||
|         <div class="alert alert-mempool">You are currently on the waitlist</div> | ||||
|       </div> | ||||
| 
 | ||||
|       <h5 i18n="accelerator.your-transaction">Your transaction</h5> | ||||
|       <div class="row"> | ||||
|         <div class="col"> | ||||
|           <small *ngIf="hasAncestors" class="form-text text-muted mb-2"> | ||||
|             <ng-container i18n="accelerator.plus-unconfirmed-ancestors">Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s)</ng-container> | ||||
|           </small> | ||||
|           <table class="table table-borderless table-border table-dark table-background table-accelerator"> | ||||
|             <tbody> | ||||
|               <tr class="group-first"> | ||||
|                 <td class="item" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td> | ||||
|                 <td style="text-align: end;" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td> | ||||
|               </tr> | ||||
|               <tr class="info"> | ||||
|                 <td class="info" colspan=3> | ||||
|                   <i><small i18n="accelerator.transaction-vbytes-size-description">Size in vbytes of this transaction (including unconfirmed ancestors)</small></i> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td class="item" i18n="accelerator.in-band-fees">In-band fees</td> | ||||
|                 <td style="text-align: end;"> | ||||
|                   {{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr class="info group-last"> | ||||
|                 <td class="info" colspan=3> | ||||
|                   <i><small i18n="accelerator.fees-already-paid-description">Fees already paid by this transaction (including unconfirmed ancestors)</small></i> | ||||
|                 </td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       </div> | ||||
|       <br> | ||||
|       <h5 *ngIf="estimate?.pools?.length" i18n="accelerator.how-much-faster">How much faster?</h5> | ||||
|       <div class="row"> | ||||
|         <div class="col"> | ||||
|           <small class="form-text text-muted mb-2" i18n="accelerator.hashrate-percentage-description">Your transaction will be prioritized by up to {{ hashratePercentage | number : '1.1-1' }}% of miners.</small> | ||||
|           <small class="form-text text-muted mb-2" i18n="accelerator.time-estimate-description">This will reduce your expected waiting time until the first confirmation to <app-time kind="within" [time]="acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time></small> | ||||
|         </div> | ||||
|         <div class="col pie"> | ||||
|           <app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box> | ||||
|         </div> | ||||
|       </div> | ||||
|       <br> | ||||
|       <h5 i18n="accelerator.pay-how-much">How much more are you willing to pay?</h5> | ||||
|       <div class="row"> | ||||
|         <div class="col"> | ||||
|           <small class="form-text text-muted mb-2" i18n="accelerator.transaction-fee-description">Choose the maximum extra transaction fee you're willing to pay.</small> | ||||
|           <div class="form-group"> | ||||
|             <div class="fee-card"> | ||||
|               <div class="d-flex mb-0"> | ||||
|                 <ng-container *ngFor="let option of maxRateOptions"> | ||||
|                   <button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)"> | ||||
|                     <span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | number }} <span class="symbol" i18n="shared.sats">sats</span></span> | ||||
|                     <span class="rate">~<app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span> | ||||
|                   </button> | ||||
|                 </ng-container> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|    | ||||
|       <h5>Acceleration summary</h5> | ||||
|       <div class="row mb-3"> | ||||
|         <div class="col"> | ||||
|           <table class="table table-borderless table-border table-dark table-background table-accelerator"> | ||||
|             <tbody> | ||||
|               <!-- ESTIMATED FEE --> | ||||
|               <ng-container> | ||||
|                 <tr class="group-first"> | ||||
|                   <td class="item" i18n="accelerator.next-block-rate">Next block market rate</td> | ||||
|                   <td class="amt" style="font-size: 16px"> | ||||
|                     {{ estimate.targetFeeRate | number : '1.0-0' }} | ||||
|                   </td> | ||||
|                   <td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td> | ||||
|                 </tr> | ||||
|                 <tr class="info"> | ||||
|                   <td class="info"> | ||||
|                     <i><small i18n="accelerator.estimated-extra-fee-required">Estimated extra fee required</small></i> | ||||
|                   </td> | ||||
|                   <td class="amt"> | ||||
|                     {{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }} | ||||
|                   </td> | ||||
|                   <td class="units"> | ||||
|                     <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                     <span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               </ng-container> | ||||
|    | ||||
|               <!-- MEMPOOL BASE FEE --> | ||||
|               <tr> | ||||
|                 <td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator™ fees</td> | ||||
|               </tr> | ||||
|               <tr class="info"> | ||||
|                 <td class="info"> | ||||
|                   <i><small i18n="accelerator.service-fee">Accelerator Service Fee</small></i> | ||||
|                 </td> | ||||
|                 <td class="amt"> | ||||
|                   +{{ estimate.mempoolBaseFee | number }} | ||||
|                 </td> | ||||
|                 <td class="units"> | ||||
|                   <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                   <span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr class="info group-last"> | ||||
|                 <td class="info"> | ||||
|                   <i><small i18n="accelerator.tx-size-surcharge">Transaction Size Surcharge</small></i> | ||||
|                 </td> | ||||
|                 <td class="amt"> | ||||
|                   +{{ estimate.vsizeFee | number }} | ||||
|                 </td> | ||||
|                 <td class="units"> | ||||
|                   <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                   <span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span> | ||||
|                 </td> | ||||
|               </tr> | ||||
| 
 | ||||
|                | ||||
|               <!-- NEXT BLOCK ESTIMATE --> | ||||
|               <ng-container> | ||||
|                 <tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;"> | ||||
|                   <td class="item"> | ||||
|                     <b style="background-color: #5E35B1" class="p-1 pl-0" i18n="accelerator.estimated-cost">Estimated acceleration cost</b> | ||||
|                   </td> | ||||
|                   <td class="amt"> | ||||
|                     <span style="background-color: #5E35B1" class="p-1 pl-0"> | ||||
|                       {{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }} | ||||
|                     </span> | ||||
|                   </td> | ||||
|                   <td class="units"> | ||||
|                     <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                     <span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|                 <tr class="info group-last" style="border-bottom: 1px solid lightgrey"> | ||||
|                   <td class="info" colspan=3> | ||||
|                     <i><small><ng-container *ngTemplateOutlet="acceleratedTo; context: {$implicit: estimate.targetFeeRate }"></ng-container></small></i> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               </ng-container> | ||||
| 
 | ||||
|               <!-- MAX COST --> | ||||
|               <ng-container> | ||||
|                 <tr class="group-first"> | ||||
|                   <td class="item"> | ||||
|                     <b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.maximum-cost">Maximum acceleration cost</b> | ||||
|                   </td> | ||||
|                   <td class="amt"> | ||||
|                     <span style="background-color: var(--primary)" class="p-1 pl-0"> | ||||
|                       {{ maxCost | number }} | ||||
|                     </span> | ||||
|                   </td> | ||||
|                   <td class="units"> | ||||
|                     <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                     <span class="fiat ml-1"> | ||||
|                       <app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat> | ||||
|                     </span> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|                 <tr class="info group-last"> | ||||
|                   <td class="info" colspan=3> | ||||
|                     <i><small><ng-container *ngTemplateOutlet="acceleratedTo; context: {$implicit: (estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize }"></ng-container></small></i> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               </ng-container> | ||||
|    | ||||
|               <!-- USER BALANCE --> | ||||
|               <ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost"> | ||||
|                 <tr class="group-first group-last" style="border-top: 1px dashed grey"> | ||||
|                   <td class="item" i18n="accelerator.available-balance">Available balance</td> | ||||
|                   <td class="amt"> | ||||
|                     {{ estimate.userBalance | number }} | ||||
|                   </td> | ||||
|                   <td class="units"> | ||||
|                     <span class="symbol" i18n="shared.sats">sats</span> | ||||
|                     <span class="fiat ml-1"> | ||||
|                       <app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat> | ||||
|                     </span> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               </ng-container> | ||||
| 
 | ||||
|               <!-- LOGIN CTA --> | ||||
|               <ng-container *ngIf="stateService.isMempoolSpaceBuild && !isLoggedIn()"> | ||||
|                 <tr class="group-first group-last" style="border-top: 1px dashed grey"> | ||||
|                   <td class="item"></td> | ||||
|                   <td class="amt"></td> | ||||
|                   <td class="units d-flex"> | ||||
|                     <a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1" i18n="shared.sign-in">Sign In</a> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               </ng-container> | ||||
|               <ng-container *ngIf="!stateService.isMempoolSpaceBuild"> | ||||
|                 <tr class="group-first group-last" style="border-top: 1px dashed grey"> | ||||
|                   <td class="item"></td> | ||||
|                   <td class="amt"></td> | ||||
|                   <td class="units d-flex"> | ||||
|                     <a [href]="'https://mempool.space/tx/' + tx.txid + '#accelerate'" class="btn btn-purple flex-grow-1" i18n="accelerator.accelerate-on-mempoolspace">Accelerate on mempool.space</a> | ||||
|                   </td> | ||||
|                 </tr> | ||||
|               </ng-container> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       </div> | ||||
|    | ||||
|       <div class="row mb-3" *ngIf="isLoggedIn()"> | ||||
|         <div class="col"> | ||||
|           <div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess"> | ||||
|             <button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()" i18n="transaction.accelerate|Accelerate button label">Accelerate</button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|    | ||||
|     </div> | ||||
|   </ng-container> | ||||
| </div> | ||||
| 
 | ||||
| <ng-template #loadingEstimate> | ||||
|   <div class="skeleton-loader"></div> | ||||
|   <br> | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #acceleratedTo let-i i18n="accelerator.accelerated-to-description">If your tx is accelerated to ~{{ i | number : '1.0-0' }} sat/vB</ng-template> | ||||
| @ -1,121 +0,0 @@ | ||||
| .fee-card { | ||||
|   padding: 15px; | ||||
|   background-color: var(--bg); | ||||
| 
 | ||||
|   .feerate { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| 
 | ||||
|     .rate { | ||||
|       font-size: 0.9em; | ||||
|       .symbol { | ||||
|         color: white; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .btn-border { | ||||
|   border: solid 1px black; | ||||
|   background-color: #0c4a87; | ||||
| } | ||||
| 
 | ||||
| .feerate.active { | ||||
|   background-color: var(--primary) !important; | ||||
|   opacity: 1; | ||||
|   border: 1px solid #007fff !important; | ||||
| } | ||||
| .feerate:focus { | ||||
|   box-shadow: none !important; | ||||
| } | ||||
| 
 | ||||
| .estimateDisabled { | ||||
|   opacity: 0.5; | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| .table-toggle { | ||||
|   width: 100%; | ||||
|   margin-top: 0.5em; | ||||
| } | ||||
| 
 | ||||
| .tab { | ||||
|   &:first-child { | ||||
|     margin-right: 1px; | ||||
|   } | ||||
|   border: solid 1px black; | ||||
|   border-bottom: none; | ||||
|   background-color: #323655; | ||||
|   border-top-left-radius: 10px !important; | ||||
|   border-top-right-radius: 10px !important; | ||||
| } | ||||
| .tab.active { | ||||
|   background-color: #5d659d !important; | ||||
|   opacity: 1; | ||||
| } | ||||
| .tab:focus { | ||||
|   box-shadow: none !important; | ||||
| } | ||||
| 
 | ||||
| .table-accelerator { | ||||
|   tr { | ||||
|     td { | ||||
|       padding-top: 0; | ||||
|       padding-bottom: 0; | ||||
|       vertical-align: baseline; | ||||
|     } | ||||
| 
 | ||||
|     &.group-first { | ||||
|       td { | ||||
|         padding-top: 0.75rem; | ||||
|       } | ||||
|     } | ||||
|     &.group-last { | ||||
|       td { | ||||
|         padding-bottom: 0.75rem; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   td { | ||||
|     &:first-child { | ||||
|       width: 100vw; | ||||
|     } | ||||
|     &.info { | ||||
|       color: #6c757d; | ||||
|       white-space: initial; | ||||
|     } | ||||
|     &.amt { | ||||
|       text-align: right; | ||||
|       padding-right: 0.2em; | ||||
|     } | ||||
|     &.units { | ||||
|       padding-left: 0.2em; | ||||
|       white-space: nowrap; | ||||
|       display: flex; | ||||
|       justify-content: space-between; | ||||
|       align-items: center; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .accelerate-cols { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   align-items: stretch; | ||||
|   margin-top: 1em; | ||||
| } | ||||
| 
 | ||||
| .col.pie { | ||||
|   flex-grow: 0; | ||||
|   padding: 0 1em; | ||||
| } | ||||
| 
 | ||||
| .item { | ||||
|   white-space: initial; | ||||
| } | ||||
| 
 | ||||
| .table-background { | ||||
|   background-color: var(--bg); | ||||
| } | ||||
| @ -1,291 +0,0 @@ | ||||
| import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core'; | ||||
| import { Subscription, catchError, of, tap } from 'rxjs'; | ||||
| import { StorageService } from '../../services/storage.service'; | ||||
| import { Transaction } from '../../interfaces/electrs.interface'; | ||||
| import { nextRoundNumber } from '../../shared/common.utils'; | ||||
| import { ServicesApiServices } from '../../services/services-api.service'; | ||||
| import { AudioService } from '../../services/audio.service'; | ||||
| import { StateService } from '../../services/state.service'; | ||||
| import { MiningStats } from '../../services/mining.service'; | ||||
| import { EtaService } from '../../services/eta.service'; | ||||
| import { DifficultyAdjustment, MempoolPosition, SinglePoolStats } from '../../interfaces/node-api.interface'; | ||||
| 
 | ||||
| export type AccelerationEstimate = { | ||||
|   txSummary: TxSummary; | ||||
|   nextBlockFee: number; | ||||
|   targetFeeRate: number; | ||||
|   userBalance: number; | ||||
|   enoughBalance: boolean; | ||||
|   cost: number; | ||||
|   mempoolBaseFee: number; | ||||
|   vsizeFee: number; | ||||
| } | ||||
| export type TxSummary = { | ||||
|   txid: string; // txid of the current transaction
 | ||||
|   effectiveVsize: number; // Total vsize of the dependency tree
 | ||||
|   effectiveFee: number;  // Total fee of the dependency tree in sats
 | ||||
|   ancestorCount: number; // Number of ancestors
 | ||||
| } | ||||
| 
 | ||||
| export interface RateOption { | ||||
|   fee: number; | ||||
|   rate: number; | ||||
|   index: number; | ||||
| } | ||||
| 
 | ||||
| export const MIN_BID_RATIO = 1; | ||||
| export const DEFAULT_BID_RATIO = 2; | ||||
| export const MAX_BID_RATIO = 4; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-accelerate-preview', | ||||
|   templateUrl: 'accelerate-preview.component.html', | ||||
|   styleUrls: ['accelerate-preview.component.scss'] | ||||
| }) | ||||
| export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges { | ||||
|   @Input() tx: Transaction; | ||||
|   @Input() mempoolPosition: MempoolPosition; | ||||
|   @Input() miningStats: MiningStats; | ||||
|   @Input() scrollEvent: boolean; | ||||
| 
 | ||||
|   math = Math; | ||||
|   error = ''; | ||||
|   showSuccess = false; | ||||
|   estimateSubscription: Subscription; | ||||
|   accelerationSubscription: Subscription; | ||||
|   difficultySubscription: Subscription; | ||||
|   da: DifficultyAdjustment; | ||||
|   estimate: any; | ||||
|   hashratePercentage?: number; | ||||
|   ETA?: number; | ||||
|   acceleratedETA?: number; | ||||
|   hasAncestors: boolean = false; | ||||
|   minExtraCost = 0; | ||||
|   minBidAllowed = 0; | ||||
|   maxBidAllowed = 0; | ||||
|   defaultBid = 0; | ||||
|   maxCost = 0; | ||||
|   userBid = 0; | ||||
|   accelerationUUID: string; | ||||
|   selectFeeRateIndex = 1; | ||||
|   isMobile: boolean = window.innerWidth <= 767.98; | ||||
|   user: any = undefined; | ||||
| 
 | ||||
|   maxRateOptions: RateOption[] = []; | ||||
| 
 | ||||
|   constructor( | ||||
|     public stateService: StateService, | ||||
|     private servicesApiService: ServicesApiServices, | ||||
|     private storageService: StorageService, | ||||
|     private etaService: EtaService, | ||||
|     private audioService: AudioService, | ||||
|     private cd: ChangeDetectorRef | ||||
|   ) { | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     if (this.estimateSubscription) { | ||||
|       this.estimateSubscription.unsubscribe(); | ||||
|     } | ||||
|     this.difficultySubscription.unsubscribe(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.accelerationUUID = window.crypto.randomUUID(); | ||||
|     this.difficultySubscription = this.stateService.difficultyAdjustment$.subscribe(da => { | ||||
|       this.da = da; | ||||
|       this.updateETA(); | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     if (changes.scrollEvent) { | ||||
|       this.scrollToPreview('acceleratePreviewAnchor', 'start'); | ||||
|     } | ||||
|     if (changes.miningStats || changes.mempoolPosition) { | ||||
|       this.updateETA(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngAfterViewInit() { | ||||
|     this.user = this.storageService.getAuth()?.user ?? null; | ||||
| 
 | ||||
|     this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe( | ||||
|       tap((response) => { | ||||
|         if (response.status === 204) { | ||||
|           this.estimate = undefined; | ||||
|           this.error = `cannot_accelerate_tx`; | ||||
|           this.scrollToPreviewWithTimeout('mempoolError', 'center'); | ||||
|           this.estimateSubscription.unsubscribe(); | ||||
|         } else { | ||||
|           this.estimate = response.body; | ||||
|           if (!this.estimate) { | ||||
|             this.error = `cannot_accelerate_tx`; | ||||
|             this.scrollToPreviewWithTimeout('mempoolError', 'center'); | ||||
|             this.estimateSubscription.unsubscribe(); | ||||
|           } | ||||
| 
 | ||||
|           if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) { | ||||
|             if (this.isLoggedIn()) { | ||||
|               this.error = `not_enough_balance`; | ||||
|               this.scrollToPreviewWithTimeout('mempoolError', 'center'); | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           this.updateETA(); | ||||
| 
 | ||||
|           this.hasAncestors = this.estimate.txSummary.ancestorCount > 1; | ||||
|            | ||||
|           // Make min extra fee at least 50% of the current tx fee
 | ||||
|           this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee)); | ||||
| 
 | ||||
|           this.maxRateOptions = [1, 2, 4].map((multiplier, index) => { | ||||
|             return { | ||||
|               fee: this.minExtraCost * multiplier, | ||||
|               rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize, | ||||
|               index, | ||||
|             }; | ||||
|           }); | ||||
| 
 | ||||
|           this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO; | ||||
|           this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO; | ||||
|           this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO; | ||||
| 
 | ||||
|           this.userBid = this.defaultBid; | ||||
|           if (this.userBid < this.minBidAllowed) { | ||||
|             this.userBid = this.minBidAllowed; | ||||
|           } else if (this.userBid > this.maxBidAllowed) { | ||||
|             this.userBid = this.maxBidAllowed; | ||||
|           }             | ||||
|           this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; | ||||
| 
 | ||||
|           if (!this.error) { | ||||
|             this.scrollToPreview('acceleratePreviewAnchor', 'start'); | ||||
| 
 | ||||
|             setTimeout(() => { | ||||
|               this.onScroll(); | ||||
|             }, 100); | ||||
|           } | ||||
|         } | ||||
|       }), | ||||
|       catchError((response) => { | ||||
|         this.estimate = undefined; | ||||
|         this.error = response.error; | ||||
|         this.scrollToPreviewWithTimeout('mempoolError', 'center'); | ||||
|         this.estimateSubscription.unsubscribe(); | ||||
|         return of(null); | ||||
|       }) | ||||
|     ).subscribe(); | ||||
|   } | ||||
| 
 | ||||
|   updateETA(): void { | ||||
|     if (!this.mempoolPosition || !this.estimate?.pools?.length || !this.miningStats || !this.da) { | ||||
|       this.hashratePercentage = undefined; | ||||
|       this.ETA = undefined; | ||||
|       this.acceleratedETA = undefined; | ||||
|       return; | ||||
|     } | ||||
|     const pools: { [id: number]: SinglePoolStats } = {}; | ||||
|     for (const pool of this.miningStats.pools) { | ||||
|       pools[pool.poolUniqueId] = pool; | ||||
|     } | ||||
| 
 | ||||
|     let totalAcceleratedHashrate = 0; | ||||
|     for (const poolId of this.estimate.pools) { | ||||
|       const pool = pools[poolId]; | ||||
|       if (!pool) { | ||||
|         continue; | ||||
|       } | ||||
|       totalAcceleratedHashrate += pool.lastEstimatedHashrate; | ||||
|     } | ||||
|     const acceleratingHashrateFraction = (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) | ||||
|     this.hashratePercentage = acceleratingHashrateFraction * 100; | ||||
| 
 | ||||
|     this.ETA = Date.now() + this.da.timeAvg * this.mempoolPosition.block; | ||||
|     this.acceleratedETA = this.etaService.calculateETAFromShares([ | ||||
|       { block: this.mempoolPosition.block, hashrateShare: (1 - acceleratingHashrateFraction) },  | ||||
|       { block: 0, hashrateShare: acceleratingHashrateFraction }, | ||||
|     ], this.da).time; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * User changed his bid | ||||
|    */ | ||||
|   setUserBid({ fee, index }: { fee: number, index: number}) { | ||||
|     if (this.estimate) { | ||||
|       this.selectFeeRateIndex = index; | ||||
|       this.userBid = Math.max(0, fee); | ||||
|       this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Scroll to element id with or without setTimeout | ||||
|    */ | ||||
|   scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) { | ||||
|     setTimeout(() => { | ||||
|       this.scrollToPreview(id, position); | ||||
|     }, 100); | ||||
|   } | ||||
|   scrollToPreview(id: string, position: ScrollLogicalPosition) { | ||||
|     const acceleratePreviewAnchor = document.getElementById(id); | ||||
|     if (acceleratePreviewAnchor) { | ||||
|       this.cd.markForCheck(); | ||||
|       acceleratePreviewAnchor.scrollIntoView({ | ||||
|         behavior: 'smooth', | ||||
|         inline: position, | ||||
|         block: position, | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Send acceleration request | ||||
|    */ | ||||
|   accelerate() { | ||||
|     if (this.accelerationSubscription) { | ||||
|       this.accelerationSubscription.unsubscribe(); | ||||
|     } | ||||
|     this.accelerationSubscription = this.servicesApiService.accelerate$( | ||||
|       this.tx.txid, | ||||
|       this.userBid, | ||||
|       this.accelerationUUID | ||||
|     ).subscribe({ | ||||
|       next: () => { | ||||
|         this.audioService.playSound('ascend-chime-cartoon'); | ||||
|         this.showSuccess = true; | ||||
|         this.scrollToPreviewWithTimeout('successAlert', 'center'); | ||||
|         this.estimateSubscription.unsubscribe(); | ||||
|       }, | ||||
|       error: (response) => { | ||||
|         if (response.status === 403 && response.error === 'not_available') { | ||||
|           this.error = 'waitlisted'; | ||||
|         } else { | ||||
|           this.error = response.error; | ||||
|         } | ||||
|         this.scrollToPreviewWithTimeout('mempoolError', 'center'); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   isLoggedIn() { | ||||
|     const auth = this.storageService.getAuth(); | ||||
|     return auth !== null; | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   onResize(): void { | ||||
|     this.isMobile = window.innerWidth <= 767.98; | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   @HostListener('window:scroll', ['$event']) // for window scroll events
 | ||||
|   onScroll() { | ||||
|     if (this.estimate) { | ||||
|       setTimeout(() => { | ||||
|         this.onScroll(); | ||||
|       }, 200); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,9 +1,11 @@ | ||||
| import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core'; | ||||
| import { BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs'; | ||||
| import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core'; | ||||
| import { BehaviorSubject, Observable, Subscription, catchError, of, switchMap, tap, throttleTime } from 'rxjs'; | ||||
| import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; | ||||
| import { StateService } from '../../../services/state.service'; | ||||
| import { WebsocketService } from '../../../services/websocket.service'; | ||||
| import { ServicesApiServices } from '../../../services/services-api.service'; | ||||
| import { SeoService } from '../../../services/seo.service'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-accelerations-list', | ||||
| @ -25,25 +27,44 @@ export class AccelerationsListComponent implements OnInit, OnDestroy { | ||||
|   maxSize = window.innerWidth <= 767.98 ? 3 : 5; | ||||
|   skeletonLines: number[] = []; | ||||
|   pageSubject: BehaviorSubject<number> = new BehaviorSubject(this.page); | ||||
|   keyNavigationSubscription: Subscription; | ||||
|   dir: 'rtl' | 'ltr' = 'ltr'; | ||||
|   paramSubscription: Subscription; | ||||
| 
 | ||||
|   constructor( | ||||
|     private servicesApiService: ServicesApiServices, | ||||
|     private websocketService: WebsocketService, | ||||
|     public stateService: StateService, | ||||
|     private cd: ChangeDetectorRef, | ||||
|     private seoService: SeoService, | ||||
|     private route: ActivatedRoute, | ||||
|     private router: Router, | ||||
|     @Inject(LOCALE_ID) private locale: string, | ||||
|   ) { | ||||
|     if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) { | ||||
|       this.dir = 'rtl'; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     if (!this.widget) { | ||||
|       this.websocketService.want(['blocks']); | ||||
|       this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`); | ||||
|     } | ||||
| 
 | ||||
|     this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; | ||||
|     this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; | ||||
|      | ||||
|     this.paramSubscription = this.route.params.pipe( | ||||
|       tap(params => { | ||||
|         this.page = +params['page'] || 1; | ||||
|         this.pageSubject.next(this.page); | ||||
|       }) | ||||
|     ).subscribe(); | ||||
| 
 | ||||
|     this.accelerationList$ = this.pageSubject.pipe( | ||||
|       switchMap((page) => { | ||||
|         this.isLoading = true; | ||||
|         const accelerationObservable$ = this.accelerations$ || (this.pending ? this.stateService.liveAccelerations$ : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page })); | ||||
|         if (!this.accelerations$ && this.pending) { | ||||
|           this.websocketService.ensureTrackAccelerations(); | ||||
| @ -79,10 +100,30 @@ export class AccelerationsListComponent implements OnInit, OnDestroy { | ||||
|         ); | ||||
|       }) | ||||
|     ); | ||||
| 
 | ||||
|     this.keyNavigationSubscription = this.stateService.keyNavigation$.pipe( | ||||
|       tap((event) => { | ||||
|         const prevKey = this.dir === 'ltr' ? 'ArrowLeft' : 'ArrowRight'; | ||||
|         const nextKey = this.dir === 'ltr' ? 'ArrowRight' : 'ArrowLeft'; | ||||
|         if (event.key === prevKey && this.page > 1) { | ||||
|           this.page--; | ||||
|           this.isLoading = true; | ||||
|           this.cd.markForCheck(); | ||||
|         } | ||||
|         if (event.key === nextKey && this.page * 15 < this.accelerationCount) { | ||||
|           this.page++; | ||||
|           this.isLoading = true; | ||||
|           this.cd.markForCheck(); | ||||
|         } | ||||
|       }), | ||||
|       throttleTime(1000, undefined, { leading: true, trailing: true }), | ||||
|     ).subscribe(() => { | ||||
|       this.pageChange(this.page); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   pageChange(page: number): void { | ||||
|     this.pageSubject.next(page); | ||||
|     this.router.navigate(['acceleration', 'list', page]); | ||||
|   } | ||||
| 
 | ||||
|   trackByBlock(index: number, block: BlockExtended): number { | ||||
| @ -91,5 +132,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.websocketService.stopTrackAccelerations(); | ||||
|     this.paramSubscription?.unsubscribe(); | ||||
|     this.keyNavigationSubscription?.unsubscribe(); | ||||
|   } | ||||
| } | ||||
| @ -4,8 +4,11 @@ | ||||
| <table> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td class="td-width field-label" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td> | ||||
|       <td class="field-value"> | ||||
|       <td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td> | ||||
|       <td class="pie-chart" rowspan="2" *ngIf="chartPositionLeft"> | ||||
|         <ng-container *ngTemplateOutlet="pieChart"></ng-container> | ||||
|       </td> | ||||
|       <td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''"> | ||||
|         <div class="effective-fee-container"> | ||||
|           @if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) { | ||||
|             <app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate> | ||||
| @ -14,7 +17,7 @@ | ||||
|           } | ||||
|         </div> | ||||
|       </td> | ||||
|       <td class="pie-chart" rowspan="2"> | ||||
|       <td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft"> | ||||
|         <ng-container *ngTemplateOutlet="pieChart"></ng-container> | ||||
|       </td> | ||||
|     </tr> | ||||
|  | ||||
| @ -16,6 +16,9 @@ | ||||
|     width: auto; | ||||
|     min-width: auto; | ||||
|   } | ||||
|   &.chart-left { | ||||
|     width: 100%; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .field-value { | ||||
| @ -23,6 +26,10 @@ | ||||
|     width: 100%; | ||||
|   } | ||||
| 
 | ||||
|   &.chart-left { | ||||
|     width: auto; | ||||
|   } | ||||
| 
 | ||||
|   .hashrate-label { | ||||
|     @media (max-width: 420px) { | ||||
|       display: none; | ||||
| @ -47,4 +54,11 @@ | ||||
|   @media (max-width: 420px) { | ||||
|     padding-left: 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| ::ng-deep .chart { | ||||
|   overflow: visible; | ||||
|   & > div, & > div > svg { | ||||
|     overflow: visible !important; | ||||
|   } | ||||
| } | ||||
| @ -4,6 +4,17 @@ import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.inte | ||||
| import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts'; | ||||
| import { MiningStats } from '../../../services/mining.service'; | ||||
| 
 | ||||
| function lighten(color, p): { r, g, b } { | ||||
|   return { | ||||
|     r: color.r + ((255 - color.r) * p), | ||||
|     g: color.g + ((255 - color.g) * p), | ||||
|     b: color.b + ((255 - color.b) * p), | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function toRGB({r,g,b}): string { | ||||
|   return `rgb(${r},${g},${b})`; | ||||
| } | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-active-acceleration-box', | ||||
| @ -17,6 +28,7 @@ export class ActiveAccelerationBox implements OnChanges { | ||||
|   @Input() miningStats: MiningStats; | ||||
|   @Input() pools: number[]; | ||||
|   @Input() chartOnly: boolean = false; | ||||
|   @Input() chartPositionLeft: boolean = false; | ||||
| 
 | ||||
|   acceleratedByPercentage: string = ''; | ||||
| 
 | ||||
| @ -43,57 +55,33 @@ export class ActiveAccelerationBox implements OnChanges { | ||||
|       pools[pool.poolUniqueId] = pool; | ||||
|     } | ||||
| 
 | ||||
|     const getDataItem = (value, color, tooltip) => ({ | ||||
|     const getDataItem = (value, color, tooltip, emphasis) => ({ | ||||
|       value, | ||||
|       name: tooltip, | ||||
|       itemStyle: { | ||||
|         color, | ||||
|         borderColor: 'rgba(0,0,0,0)', | ||||
|         borderWidth: 1, | ||||
|       }, | ||||
|       avoidLabelOverlap: false, | ||||
|       label: { | ||||
|         show: false, | ||||
|       }, | ||||
|       labelLine: { | ||||
|         show: false | ||||
|       }, | ||||
|       emphasis: { | ||||
|         disabled: true, | ||||
|       }, | ||||
|       tooltip: { | ||||
|         show: true, | ||||
|         backgroundColor: 'rgba(17, 19, 31, 1)', | ||||
|         borderRadius: 4, | ||||
|         shadowColor: 'rgba(0, 0, 0, 0.5)', | ||||
|         textStyle: { | ||||
|           color: 'var(--tooltip-grey)', | ||||
|         }, | ||||
|         borderColor: '#000', | ||||
|         formatter: () => { | ||||
|           return tooltip; | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     let totalAcceleratedHashrate = 0; | ||||
|     for (const poolId of poolList || []) { | ||||
|     const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate); | ||||
|     const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0); | ||||
|     acceleratingPools.forEach((poolId, index) => { | ||||
|       const pool = pools[poolId]; | ||||
|       if (!pool) { | ||||
|         continue; | ||||
|       } | ||||
|       totalAcceleratedHashrate += pool.lastEstimatedHashrate; | ||||
|     } | ||||
|       const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1); | ||||
|       data.push(getDataItem( | ||||
|         pool.lastEstimatedHashrate, | ||||
|         toRGB(lighten({ r: 147, g: 57, b: 244 }, index * .08)), | ||||
|         `<b style="color: white">${pool.name} (${poolShare}%)</b>`, | ||||
|         true, | ||||
|       ) as PieSeriesOption); | ||||
|     }) | ||||
|     this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%'; | ||||
|     data.push(getDataItem( | ||||
|       totalAcceleratedHashrate, | ||||
|       'var(--mainnet-alt)', | ||||
|       `${this.acceleratedByPercentage} accelerating`, | ||||
|     ) as PieSeriesOption); | ||||
|     const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%'; | ||||
|     data.push(getDataItem( | ||||
|       (this.miningStats.lastEstimatedHashrate - totalAcceleratedHashrate), | ||||
|       'rgba(127, 127, 127, 0.3)', | ||||
|       `${notAcceleratedByPercentage} not accelerating`, | ||||
|       `not accelerating (${notAcceleratedByPercentage})`, | ||||
|       false, | ||||
|     ) as PieSeriesOption); | ||||
| 
 | ||||
|     return data; | ||||
| @ -111,11 +99,28 @@ export class ActiveAccelerationBox implements OnChanges { | ||||
|       tooltip: { | ||||
|         show: true, | ||||
|         trigger: 'item', | ||||
|         backgroundColor: 'rgba(17, 19, 31, 1)', | ||||
|         borderRadius: 4, | ||||
|         shadowColor: 'rgba(0, 0, 0, 0.5)', | ||||
|         textStyle: { | ||||
|           color: 'var(--tooltip-grey)', | ||||
|         }, | ||||
|         borderColor: '#000', | ||||
|         formatter: (item) => { | ||||
|           return item.name; | ||||
|         } | ||||
|       }, | ||||
|       series: [ | ||||
|         { | ||||
|           type: 'pie', | ||||
|           radius: '100%', | ||||
|           label: { | ||||
|             show: false | ||||
|           }, | ||||
|           labelLine: { | ||||
|             show: false | ||||
|           }, | ||||
|           animationDuration: 0, | ||||
|           data: this.getChartData(pools), | ||||
|         } | ||||
|       ] | ||||
|  | ||||
| @ -26,11 +26,13 @@ | ||||
|               <tbody> | ||||
|                 <tr><ng-container *ngTemplateOutlet="balanceRow"></ng-container></tr> | ||||
|                 <tr><ng-container *ngTemplateOutlet="pendingBalanceRow"></ng-container></tr> | ||||
|                 <tr><ng-container *ngTemplateOutlet="utxoRow"></ng-container></tr> | ||||
|                 <tr><ng-container *ngTemplateOutlet="pendingUtxoRow"></ng-container></tr> | ||||
|                 @if (!address.electrum) { | ||||
|                   <tr><ng-container *ngTemplateOutlet="utxoRow"></ng-container></tr> | ||||
|                   <tr><ng-container *ngTemplateOutlet="pendingUtxoRow"></ng-container></tr> | ||||
|                 } | ||||
|                 @if (network === 'liquid' || network === 'liquidtestnet') { | ||||
|                   <tr><ng-container *ngTemplateOutlet="liquidRow"></ng-container></tr> | ||||
|                 } @else { | ||||
|                 } @else if (!address.electrum) { | ||||
|                   <tr><ng-container *ngTemplateOutlet="volumeRow"></ng-container></tr> | ||||
|                 } | ||||
|                 <tr><ng-container *ngTemplateOutlet="typeRow"></ng-container></tr> | ||||
| @ -46,17 +48,21 @@ | ||||
|                   <ng-container *ngTemplateOutlet="spacerCell"></ng-container> | ||||
|                   <ng-container *ngTemplateOutlet="pendingBalanceRow"></ng-container> | ||||
|                 </tr> | ||||
|                 @if (!address.electrum) { | ||||
|                 <tr> | ||||
|                   <ng-container *ngTemplateOutlet="utxoRow"></ng-container> | ||||
|                   <ng-container *ngTemplateOutlet="spacerCell"></ng-container> | ||||
|                   <ng-container *ngTemplateOutlet="pendingUtxoRow"></ng-container> | ||||
|                 </tr> | ||||
|                 } | ||||
|                 <tr> | ||||
|                   @if (network === 'liquid' || network === 'liquidtestnet') { | ||||
|                     <ng-container *ngTemplateOutlet="liquidRow"></ng-container> | ||||
|                   } @else { | ||||
|                   } @else if (!address.electrum) { | ||||
|                     <ng-container *ngTemplateOutlet="volumeRow"></ng-container> | ||||
|                   } | ||||
|                   } @else { | ||||
|                     <ng-container *ngTemplateOutlet="emptyTd"></ng-container> | ||||
|                   }                   | ||||
|                   <ng-container *ngTemplateOutlet="spacerCell"></ng-container> | ||||
|                   <ng-container *ngTemplateOutlet="typeRow"></ng-container> | ||||
|                 </tr> | ||||
| @ -232,6 +238,11 @@ | ||||
|   <td class="spacer"></td> | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #emptyTd> | ||||
|   <td class="spacer"></td> | ||||
|   <td class="spacer"></td> | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #balanceRow> | ||||
|   <td i18n="address.confirmed-balance">Confirmed balance</td> | ||||
|   <td *ngIf="chainStats.funded_txo_sum !== undefined; else confidentialTd" class="wrap-cell"><app-amount [satoshis]="chainStats.balance" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="chainStats.balance"></app-fiat></span></td> | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <ng-container *ngIf="!noFiat && (viewAmountMode$ | async) === 'fiat' && (conversions$ | async) as conversions; else viewFiatVin"> | ||||
| <ng-container *ngIf="!noFiat && ignoreViewMode === false && (viewAmountMode$ | async) === 'fiat' && (conversions$ | async) as conversions; else viewFiatVin"> | ||||
|   <span class="fiat" *ngIf="blockConversion; else noblockconversion"> | ||||
|     {{ addPlus && satoshis >= 0 ? '+' : '' }}{{ | ||||
|       ( | ||||
| @ -21,7 +21,7 @@ | ||||
|     <span i18n="shared.confidential">Confidential</span> | ||||
|   </ng-template> | ||||
|   <ng-template #default> | ||||
|     @if ((viewAmountMode$ | async) === 'btc' || (viewAmountMode$ | async) === 'fiat') { | ||||
|     @if ((viewAmountMode$ | async) === 'btc' || (viewAmountMode$ | async) === 'fiat' || ignoreViewMode === true) { | ||||
|       ‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }} | ||||
|       <span class="symbol"> | ||||
|         <ng-container *ngTemplateOutlet="prefix"></ng-container>BTC | ||||
|  | ||||
| @ -24,6 +24,7 @@ export class AmountComponent implements OnInit, OnDestroy { | ||||
|   @Input() addPlus = false; | ||||
|   @Input() blockConversion: Price; | ||||
|   @Input() forceBtc: boolean = false; | ||||
|   @Input() ignoreViewMode: boolean = false; | ||||
|   @Input() forceBlockConversion: boolean = false; // true = displays fiat price as 0 if blockConversion is undefined instead of falling back to conversions
 | ||||
| 
 | ||||
|   constructor( | ||||
|  | ||||
| @ -0,0 +1,99 @@ | ||||
| <div class="wrapper"> | ||||
| 
 | ||||
|   @if (!minimal) { | ||||
|     <span *ngIf="paymentStatus === 3" class="valid-feedback d-block mt-5"> | ||||
|       Payment successful. You can close this page. | ||||
|     </span> | ||||
| 
 | ||||
|     <span *ngIf="paymentStatus === 4" class="valid-feedback d-block mt-5"> | ||||
|       A transaction <a [href]="'/tx/' + loadedInvoice.cryptoInfo[0].payments[0].id.split('-')[0]">has been detected in the mempool</a> fully paying for this invoice. Waiting for on-chain confirmation. | ||||
|     </span> | ||||
|   } | ||||
| 
 | ||||
|   <div *ngIf="paymentStatus === 2"> | ||||
|      | ||||
|     <form [formGroup]="paymentForm"> | ||||
| 
 | ||||
|       <div *ngIf="availableMethods.length > 1" class="form-group"> | ||||
|         <div class="btn-group btn-group-toggle" data-toggle="buttons"> | ||||
|           <!-- <label *ngIf="loadedInvoice.addresses.BTC" class="btn btn-primary" [ngClass]="{'active': paymentForm.get('method')?.value === 'chain'}"> | ||||
|             <input type="radio" value="chain" formControlName="method"> <fa-icon [icon]="['fas', 'link']" [fixedWidth]="true" title="Onchain"></fa-icon> | ||||
|           </label> --> | ||||
|           <label *ngIf="loadedInvoice.addresses.BTC_LightningLike" class="btn btn-primary" [ngClass]="{'active': paymentForm.get('method')?.value === 'lightning'}"> | ||||
|             <input type="radio" value="lightning" formControlName="method"> <fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" title="Lightning"></fa-icon> | ||||
|           </label> | ||||
|           <!-- <label *ngIf="loadedInvoice.addresses.LBTC" class="btn btn-primary" [ngClass]="{'active': paymentForm.get('method')?.value === 'lbtc'}"> | ||||
|             <input type="radio" value="lbtc" formControlName="method"> <fa-icon [icon]="['fas', 'tint']" [fixedWidth]="true" title="Liquid Bitcoin"></fa-icon> | ||||
|           </label> --> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|     </form> | ||||
| 
 | ||||
|     <ng-template [ngIf]="paymentForm.get('method')?.value === 'chain' && loadedInvoice"> | ||||
| 
 | ||||
|         <div class="qr-wrapper" [class.mt-0]="minimal"> | ||||
|           <a [href]="bypassSecurityTrustUrl('bitcoin:' + loadedInvoice.addresses.BTC + '?amount=' + loadedInvoice.btcDue)" target="_blank"> | ||||
|               <app-qrcode imageUrl="/resources/bitcoin-logo.png" [size]="200" [data]="'bitcoin:' + loadedInvoice.addresses.BTC + '?amount=' + loadedInvoice.btcDue"></app-qrcode> | ||||
|           </a> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="input-group input-group-sm info-group"> | ||||
|           <input type="text" class="form-control input-dark" readonly [value]="loadedInvoice.addresses.BTC"> | ||||
|           <div class="input-group-append"> | ||||
|               <button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="loadedInvoice.addresses.BTC"></app-clipboard></button> | ||||
|           </div> | ||||
|         </div> | ||||
|         @if (!minimal) { | ||||
|           <p>{{ loadedInvoice.btcDue | number: '1.0-8' }} <span class="symbol">BTC</span></p> | ||||
|         } | ||||
| 
 | ||||
|     </ng-template> | ||||
| 
 | ||||
|     <ng-template [ngIf]="paymentForm.get('method')?.value === 'lightning' && loadedInvoice && loadedInvoice.addresses.BTC_LightningLike"> | ||||
| 
 | ||||
|         <div class="qr-wrapper" [class.mt-0]="minimal"> | ||||
|           <a [href]="bypassSecurityTrustUrl('lightning:' + loadedInvoice.addresses.BTC_LightningLike)" target="_blank"> | ||||
|               <app-qrcode imageUrl="/resources/lightning-logo.png" [size]="200" [data]="loadedInvoice.addresses.BTC_LightningLike.toUpperCase()"></app-qrcode> | ||||
|           </a> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="input-group input-group-sm info-group"> | ||||
|           <input type="text" class="form-control input-dark" readonly [value]="loadedInvoice.addresses.BTC_LightningLike"> | ||||
|           <div class="input-group-append"> | ||||
|               <button class="btn btn-outline-secondary" type="button"><app-clipboard [text]="loadedInvoice.addresses.BTC_LightningLike"></app-clipboard></button> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         @if (!minimal) { | ||||
|           <p>{{ loadedInvoice.btcDue * 100_000_000 | number: '1.0-0' }} <span class="symbol">sats</span></p> | ||||
|         } | ||||
| 
 | ||||
|     </ng-template> | ||||
| 
 | ||||
|     <ng-template [ngIf]="loadedInvoice && (paymentForm.get('method')?.value === 'lbtc' || paymentForm.get('method')?.value === 'tlbtc')"> | ||||
| 
 | ||||
|         <div class="qr-wrapper" [class.mt-0]="minimal"> | ||||
|           <a [href]="bypassSecurityTrustUrl('liquidnetwork:' + loadedInvoice.addresses.LBTC + '?amount=' + loadedInvoice.btcDue + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d')" target="_blank"> | ||||
|               <app-qrcode imageUrl="/resources/liquid-bitcoin.png" [size]="200" [data]="'liquidnetwork:' + loadedInvoice.addresses.LBTC + '?amount=' + loadedInvoice.btcDue + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'"></app-qrcode> | ||||
|           </a> | ||||
|         </div> | ||||
|         <br> | ||||
|         <div class="input-group input-group-sm info-group"> | ||||
|           <input type="text" class="form-control input-dark" readonly [value]="loadedInvoice.addresses.LBTC" /> | ||||
|           <div class="input-group-append"> | ||||
|               <button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="loadedInvoice.addresses.LBTC"></app-clipboard></button> | ||||
|           </div> | ||||
|         </div> | ||||
|         @if (!minimal) { | ||||
|           <p>{{ loadedInvoice.btcDue | number: '1.0-8' }} <span class="symbol">BTC</span></p> | ||||
|         } | ||||
| 
 | ||||
|     </ng-template> | ||||
| 
 | ||||
|     @if (!minimal) { | ||||
|       <p>Waiting for transaction... </p> | ||||
|       <div class="spinner-border text-light"></div> | ||||
|     } | ||||
|   </div> | ||||
| </div> | ||||
| @ -0,0 +1,150 @@ | ||||
| .form-panel { | ||||
|   background-color: #292b45; | ||||
|   padding: 20px; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| .sponsor-page { | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .qr-wrapper { | ||||
|     background-color: #FFF; | ||||
|     padding: 10px; | ||||
|     display: inline-block; | ||||
|     padding-bottom: 5px; | ||||
|     margin: 20px auto 0px; | ||||
| } | ||||
| 
 | ||||
| .info-group { | ||||
|   max-width: 400px; | ||||
| } | ||||
| 
 | ||||
| .card { | ||||
|   width: 240px; | ||||
|   height: 220px; | ||||
|   background-color: var(--bg); | ||||
|   border: 2px solid var(--bg); | ||||
|   cursor: pointer; | ||||
|   position: relative; | ||||
|   transition: 100ms all; | ||||
|   margin: 30px 30px 20px 30px; | ||||
|   @media(min-width: 476px) { | ||||
|     margin: 30px 100px 20px 100px; | ||||
|   } | ||||
|   @media(min-width: 851px) { | ||||
|     margin: 60px 20px 40px 20px; | ||||
|   } | ||||
| 
 | ||||
|   .card-title { | ||||
|     font-weight: bold; | ||||
|     span { | ||||
|       font-weight: 100; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &.bigger { | ||||
|     height: 220px; | ||||
|     width: 240px; | ||||
|     margin-top: 40px; | ||||
|   } | ||||
| 
 | ||||
|   &:hover { | ||||
|     background-color: #5058926b; | ||||
|     border: 2px solid #505892; | ||||
|     transform: scale(1.1) translateY(-10px); | ||||
|     margin-top: 70px; | ||||
| 
 | ||||
|     .card-header { | ||||
|       background-color: #505892; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .donation-form { | ||||
|   max-width: 280px; | ||||
|   margin: auto; | ||||
|   button { | ||||
|     width: 100%; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .card-header { | ||||
|   background-color: #171929; | ||||
| } | ||||
| 
 | ||||
| .flex-container { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   flex-wrap: wrap; | ||||
|   justify-content: center; | ||||
| } | ||||
| 
 | ||||
| .middle-card { | ||||
|   width: 280px; | ||||
|   height: 260px; | ||||
|   margin-top: 40px; | ||||
|   &:hover { | ||||
|     margin-top: 50px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .shiny-border { | ||||
|   background-color: #5058926b; | ||||
|   border: 2px solid #505892; | ||||
|   transform: scale(1.1) translateY(-10px); | ||||
|   margin-top: 70px; | ||||
|   box-shadow: 0px 0px 100px #9858ff52; | ||||
|   .card-header { | ||||
|     background-color: #505892; | ||||
|   } | ||||
| 
 | ||||
|   &.middle-card { | ||||
|     margin-top: 50px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .input-group { | ||||
|   margin: 20px auto; | ||||
| } | ||||
| 
 | ||||
| .donation-confirmed { | ||||
|   h2 { | ||||
|     margin-top: 50px; | ||||
|     span { | ||||
|       display: block; | ||||
|       &:last-child { | ||||
|         color: #9858ff; | ||||
|         font-weight: bold; | ||||
|         font-size: 2rem; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .order-details { | ||||
|     margin-top: 50px; | ||||
|     span { | ||||
|       color: #d81b60; | ||||
|       margin-left: 10px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .card-body { | ||||
|   align-items: center; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   flex-direction: column; | ||||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| .wrapper { | ||||
|   text-align: center; | ||||
|   width: 100%; | ||||
| } | ||||
| 
 | ||||
| .input-dark { | ||||
|   background-color: var(--bg); | ||||
|   border-color: var(--active-bg); | ||||
|   color: white; | ||||
| } | ||||
| @ -0,0 +1,110 @@ | ||||
| import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; | ||||
| import { FormBuilder, FormGroup } from '@angular/forms'; | ||||
| import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { Subscription, of, timer } from 'rxjs'; | ||||
| import { retry, switchMap, tap } from 'rxjs/operators'; | ||||
| import { ServicesApiServices } from '../../services/services-api.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-bitcoin-invoice', | ||||
|   templateUrl: './bitcoin-invoice.component.html', | ||||
|   styleUrls: ['./bitcoin-invoice.component.scss'] | ||||
| }) | ||||
| export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy { | ||||
|   @Input() invoice; | ||||
|   @Input() invoiceId: string; | ||||
|   @Input() redirect = true; | ||||
|   @Input() minimal = false; | ||||
|   @Output() completed = new EventEmitter(); | ||||
| 
 | ||||
|   paymentForm: FormGroup; | ||||
|   requestSubscription: Subscription | undefined; | ||||
|   paymentStatusSubscription: Subscription | undefined; | ||||
|   loadedInvoice: any; | ||||
|   paymentStatus = 1; // 1 - Waiting for invoice | 2 - Pending payment | 3 - Payment completed
 | ||||
|   paramMapSubscription: Subscription | undefined; | ||||
|   invoiceSubscription: Subscription | undefined; | ||||
|   invoiceTimeout; // Wait for angular to load all the things before making a request
 | ||||
| 
 | ||||
|   constructor( | ||||
|     private formBuilder: FormBuilder, | ||||
|     private apiService: ServicesApiServices, | ||||
|     private sanitizer: DomSanitizer, | ||||
|     private activatedRoute: ActivatedRoute | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     if (this.requestSubscription) { | ||||
|       this.requestSubscription.unsubscribe(); | ||||
|     } | ||||
|     if (this.paramMapSubscription) { | ||||
|       this.paramMapSubscription.unsubscribe(); | ||||
|     } | ||||
|     if (this.invoiceSubscription) { | ||||
|       this.invoiceSubscription.unsubscribe(); | ||||
|     } | ||||
|     if (this.paymentStatusSubscription) { | ||||
|       this.paymentStatusSubscription.unsubscribe(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   ngOnInit(): void { | ||||
|     this.paymentForm = this.formBuilder.group({ | ||||
|       'method': 'lightning' | ||||
|     }); | ||||
| 
 | ||||
|     /** | ||||
|      * If the invoice is passed in the url, fetch it and display btcpay payment | ||||
|      * Otherwise get a new invoice | ||||
|      */ | ||||
|     this.paramMapSubscription = this.activatedRoute.paramMap | ||||
|       .pipe( | ||||
|         tap((paramMap) => { | ||||
|           this.fetchInvoice(paramMap.get('invoiceId') ?? this.invoiceId); | ||||
|         }) | ||||
|       ).subscribe(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges(changes: SimpleChanges): void { | ||||
|     if ((changes.invoice || changes.invoiceId) && this.invoiceId) { | ||||
|       this.fetchInvoice(this.invoiceId); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   fetchInvoice(invoiceId: string): void { | ||||
|     if (invoiceId) { | ||||
|       if (this.paymentStatusSubscription) { | ||||
|         this.paymentStatusSubscription.unsubscribe(); | ||||
|       } | ||||
|       this.paymentStatusSubscription = ((this.invoice && this.invoice.id === invoiceId) ? of(this.invoice) : this.apiService.retreiveInvoice$(invoiceId)).pipe( | ||||
|         tap((invoice: any) => { | ||||
|           this.loadedInvoice = invoice; | ||||
|           if (this.loadedInvoice.btcDue > 0) { | ||||
|             this.paymentStatus = 2; | ||||
|           } else { | ||||
|             this.paymentStatus = 4; | ||||
|           } | ||||
|         }), | ||||
|         switchMap(() => this.apiService.getPaymentStatus$(this.loadedInvoice.id) | ||||
|           .pipe( | ||||
|             retry({ delay: () => timer(2000)}) | ||||
|           ) | ||||
|         ), | ||||
|       ).subscribe({ | ||||
|         next: ((result) => { | ||||
|           this.paymentStatus = 3; | ||||
|           this.completed.emit(); | ||||
|         }), | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   get availableMethods(): string[] { | ||||
|     return Object.keys(this.loadedInvoice?.addresses || {}).filter(k => k === 'BTC_LightningLike'); | ||||
|   } | ||||
| 
 | ||||
|   bypassSecurityTrustUrl(text: string): SafeUrl { | ||||
|     return this.sanitizer.bypassSecurityTrustUrl(text); | ||||
|   } | ||||
| } | ||||
| @ -73,27 +73,27 @@ export class BlocksList implements OnInit { | ||||
|         this.seoService.setDescription($localize`:@@meta.description.bitcoin.blocks:See the most recent Bitcoin${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block reward, block size, and more.`); | ||||
|       } | ||||
| 
 | ||||
|       this.blocksCountInitializedSubscription = combineLatest([this.blocksCountInitialized$, this.route.queryParams]).pipe( | ||||
|       this.blocksCountInitializedSubscription = combineLatest([this.blocksCountInitialized$, this.route.params]).pipe( | ||||
|         filter(([blocksCountInitialized, _]) => blocksCountInitialized), | ||||
|         tap(([_, params]) => { | ||||
|           this.page = +params['page'] || 1; | ||||
|           this.page === 1 ? this.fromHeightSubject.next(undefined) : this.fromHeightSubject.next((this.blocksCount - 1) - (this.page - 1) * 15); | ||||
|           this.cd.markForCheck(); | ||||
|         }) | ||||
|       ).subscribe(); | ||||
| 
 | ||||
|       this.keyNavigationSubscription = this.stateService.keyNavigation$ | ||||
|       .pipe( | ||||
|         tap((event) => { | ||||
|           this.isLoading = true; | ||||
|           const prevKey = this.dir === 'ltr' ? 'ArrowLeft' : 'ArrowRight'; | ||||
|           const nextKey = this.dir === 'ltr' ? 'ArrowRight' : 'ArrowLeft'; | ||||
|           if (event.key === prevKey && this.page > 1) { | ||||
|             this.page--; | ||||
|             this.isLoading = true; | ||||
|             this.cd.markForCheck(); | ||||
|           } | ||||
|           if (event.key === nextKey && this.page * 15 < this.blocksCount) { | ||||
|             this.page++; | ||||
|             this.isLoading = true; | ||||
|             this.cd.markForCheck(); | ||||
|           } | ||||
|         }), | ||||
| @ -118,6 +118,7 @@ export class BlocksList implements OnInit { | ||||
|                 if (this.blocksCount === undefined) { | ||||
|                   this.blocksCount = blocks[0].height + 1; | ||||
|                   this.blocksCountInitialized$.next(true); | ||||
|                   this.blocksCountInitialized$.complete(); | ||||
|                 } | ||||
|                 this.isLoading = false; | ||||
|                 this.lastBlockHeight = Math.max(...blocks.map(o => o.height)); | ||||
| @ -179,7 +180,7 @@ export class BlocksList implements OnInit { | ||||
|   } | ||||
| 
 | ||||
|   pageChange(page: number): void { | ||||
|     this.router.navigate([], { queryParams: { page: page } }); | ||||
|     this.router.navigate(['blocks', page]); | ||||
|   } | ||||
| 
 | ||||
|   trackByBlock(index: number, block: BlockExtended): number { | ||||
|  | ||||
| @ -36,7 +36,7 @@ export class RecentPegsListComponent implements OnInit { | ||||
|   lastPegBlockUpdate: number = 0; | ||||
|   lastPegAmount: string = ''; | ||||
|   isLoad: boolean = true; | ||||
|   queryParamSubscription: Subscription; | ||||
|   paramSubscription: Subscription; | ||||
|   keyNavigationSubscription: Subscription; | ||||
|   dir: 'rtl' | 'ltr' = 'ltr'; | ||||
| 
 | ||||
| @ -66,7 +66,7 @@ export class RecentPegsListComponent implements OnInit { | ||||
|       this.seoService.setTitle($localize`:@@a8b0889ea1b41888f1e247f2731cc9322198ca04:Recent Peg-In / Out's`); | ||||
|       this.websocketService.want(['blocks']); | ||||
| 
 | ||||
|       this.queryParamSubscription = this.route.queryParams.pipe( | ||||
|       this.paramSubscription = this.route.params.pipe( | ||||
|         tap((params) => { | ||||
|           this.page = +params['page'] || 1; | ||||
|           this.startingIndexSubject.next((this.page - 1) * 15); | ||||
| @ -76,15 +76,16 @@ export class RecentPegsListComponent implements OnInit { | ||||
|       this.keyNavigationSubscription = this.stateService.keyNavigation$ | ||||
|       .pipe( | ||||
|         tap((event) => { | ||||
|           this.isLoading = true; | ||||
|           const prevKey = this.dir === 'ltr' ? 'ArrowLeft' : 'ArrowRight'; | ||||
|           const nextKey = this.dir === 'ltr' ? 'ArrowRight' : 'ArrowLeft'; | ||||
|           if (event.key === prevKey && this.page > 1) { | ||||
|             this.page--; | ||||
|             this.isLoading = true; | ||||
|             this.cd.markForCheck(); | ||||
|           } | ||||
|           if (event.key === nextKey && this.page < this.pegsCount / this.pageSize) { | ||||
|             this.page++; | ||||
|             this.isLoading = true; | ||||
|             this.cd.markForCheck(); | ||||
|           } | ||||
|         }), | ||||
| @ -172,12 +173,12 @@ export class RecentPegsListComponent implements OnInit { | ||||
|   ngOnDestroy(): void { | ||||
|     this.destroy$.next(1); | ||||
|     this.destroy$.complete(); | ||||
|     this.queryParamSubscription?.unsubscribe(); | ||||
|     this.paramSubscription?.unsubscribe(); | ||||
|     this.keyNavigationSubscription?.unsubscribe(); | ||||
|   } | ||||
| 
 | ||||
|   pageChange(page: number): void { | ||||
|     this.router.navigate([], { queryParams: { page: page } }); | ||||
|     this.router.navigate(['audit', 'pegs', page]); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -97,7 +97,7 @@ | ||||
|                       </tr> | ||||
|                     </thead> | ||||
|                     <tbody> | ||||
|                       <td class="text-center"><app-amount [satoshis]="poolStats.totalReward" digitsInfo="1.0-0" [noFiat]="true"></app-amount></td> | ||||
|                       <td class="text-center"><app-amount [satoshis]="poolStats.totalReward" digitsInfo="1.0-0" [noFiat]="true" [ignoreViewMode]="true"></app-amount></td> | ||||
|                       <td class="text-center">{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td> | ||||
|                       <td class="text-center" *ngIf="auditAvailable; else emptyTd"><span class="health-badge badge" [class.badge-success]="poolStats.avgBlockHealth >= 99" | ||||
|                           [class.badge-warning]="poolStats.avgBlockHealth >= 75 && poolStats.avgBlockHealth < 99" [class.badge-danger]="poolStats.avgBlockHealth < 75" | ||||
| @ -146,7 +146,7 @@ | ||||
|                       </tr> | ||||
|                     </thead> | ||||
|                     <tbody> | ||||
|                       <td *ngFor="let total of oob" class="text-center clip"><app-amount [satoshis]="total.cost" [digitsInfo]="isMobile() ? '1.2-4' : '1.8-8'"></app-amount></td> | ||||
|                       <td *ngFor="let total of oob" class="text-center clip"><app-amount [satoshis]="total.cost" [digitsInfo]="isMobile() ? '1.2-4' : '1.8-8'" [ignoreViewMode]="true"></app-amount></td> | ||||
|                     </tbody> | ||||
|                   </table> | ||||
|                 </td> | ||||
| @ -219,10 +219,10 @@ | ||||
|             </ng-template> | ||||
|           </td> | ||||
|           <td class="reward text-right"> | ||||
|             <app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true"></app-amount> | ||||
|             <app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true" [ignoreViewMode]="true"></app-amount> | ||||
|           </td> | ||||
|           <td *ngIf="!auditAvailable" class="fees text-right"> | ||||
|             <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true"></app-amount> | ||||
|             <app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true" [ignoreViewMode]="true"></app-amount> | ||||
|           </td> | ||||
|           <td class="txs text-right"> | ||||
|             {{ block.tx_count | number }} | ||||
|  | ||||
| @ -1,9 +1,10 @@ | ||||
| img { | ||||
|   position: absolute; | ||||
|   top: 67px; | ||||
|   left: 67px; | ||||
|   width: 65px; | ||||
|   height: 65px; | ||||
|   top: 50%; | ||||
|   left: 50%; | ||||
|   width: 42px; | ||||
|   height: 42px; | ||||
|   transform: translate(-50%, -50%); | ||||
| } | ||||
| 
 | ||||
| .holder { | ||||
|  | ||||
| @ -37,7 +37,7 @@ export class QrcodeComponent implements AfterViewInit { | ||||
|       return; | ||||
|     } | ||||
|     const opts: QRCode.QRCodeRenderersOptions = { | ||||
|       errorCorrectionLevel: 'L', | ||||
|       errorCorrectionLevel: 'M', | ||||
|       margin: 0, | ||||
|       color: { | ||||
|         dark: '#000', | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
| 
 | ||||
|   <div class="clearfix"></div> | ||||
| 
 | ||||
|   <div class="rbf-trees" style="min-height: 295px"> | ||||
|   <div class="rbf-trees" [ngStyle]="{ 'min-height': '295px', 'opacity': isLoading ? '0.75' : '1' }"> | ||||
|     <ng-container *ngIf="rbfTrees$ | async as trees"> | ||||
|       <div *ngFor="let tree of trees" class="tree"> | ||||
|         <p class="info"> | ||||
|  | ||||
| @ -38,6 +38,7 @@ export class RbfList implements OnInit, OnDestroy { | ||||
|       this.fullRbf = (fragment === 'fullrbf'); | ||||
|       this.websocketService.startTrackRbf(this.fullRbf ? 'fullRbf' : 'all'); | ||||
|       this.nextRbfSubject.next(null); | ||||
|       this.isLoading = true; | ||||
|     }); | ||||
| 
 | ||||
|     this.rbfTrees$ = merge( | ||||
|  | ||||
| @ -78,6 +78,10 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { | ||||
|   } | ||||
| 
 | ||||
|   calculate() { | ||||
|     if (!this.time) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     let seconds: number; | ||||
|     switch (this.kind) { | ||||
|       case 'since': | ||||
|  | ||||
| @ -75,9 +75,9 @@ | ||||
|                   } @else { | ||||
|                     <app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time> | ||||
|                   } | ||||
|                   @if (!showAccelerationSummary && isMobile && paymentType === 'cashapp' && accelerationEligible && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) { | ||||
|                   <!-- @if (!showAccelerationSummary && isMobile && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) { | ||||
|                     <a class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a> | ||||
|                   } | ||||
|                   } --> | ||||
|                 </span> | ||||
|               </ng-container> | ||||
|               <ng-template #etaSkeleton> | ||||
| @ -115,8 +115,15 @@ | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="bottom-panel"> | ||||
|       @if (showAccelerationSummary && !accelerationFlowCompleted) { | ||||
|         <app-accelerate-checkout *ngIf="(da$ | async) as da;" [txid]="tx.txid" [eta]="mempoolPosition?.block >= 7 ? null : da.adjustedTimeAvg * (mempoolPosition.block + 1) + now + da.timeOffset" (close)="accelerationFlowCompleted = true" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100"></app-accelerate-checkout> | ||||
|       @if (isLoading) { | ||||
|         <div class="progress-icon"> | ||||
|           <div class="spinner-border text-light" style="width: 1em; height: 1em"></div> | ||||
|         </div> | ||||
|         <span class="explainer"> </span> | ||||
|       } @else if (showAccelerationSummary) { | ||||
|         <ng-container *ngIf="(ETA$ | async) as eta;"> | ||||
|           <app-accelerate-checkout *ngIf="(da$ | async) as da;" [cashappEnabled]="accelerationEligible" [advancedEnabled]="false" [forceMobile]="true" [tx]="tx" [miningStats]="miningStats" [eta]="eta" [scrollEvent]="scrollIntoAccelPreview" class="h-100 w-100"></app-accelerate-checkout> | ||||
|         </ng-container> | ||||
|       } @else { | ||||
|         @if (tx?.acceleration && !tx.status?.confirmed) { | ||||
|           <div class="progress-icon"> | ||||
|  | ||||
| @ -63,8 +63,9 @@ export class TrackerComponent implements OnInit, OnDestroy { | ||||
|   mempoolPosition: MempoolPosition; | ||||
|   accelerationPositions: AccelerationPosition[]; | ||||
|   isLoadingTx = true; | ||||
|   error: any = undefined; | ||||
|   loadingCachedTx = false; | ||||
|   loadingPosition = true; | ||||
|   error: any = undefined; | ||||
|   waitingForTransaction = false; | ||||
|   latestBlock: BlockExtended; | ||||
|   transactionTime = -1; | ||||
| @ -107,7 +108,6 @@ export class TrackerComponent implements OnInit, OnDestroy { | ||||
|   now = Date.now(); | ||||
|   da$: Observable<DifficultyAdjustment>; | ||||
|   isMobile: boolean; | ||||
|   paymentType: 'bitcoin' | 'cashapp' = 'bitcoin'; | ||||
| 
 | ||||
|   trackerStage: TrackerStage = 'waiting'; | ||||
| 
 | ||||
| @ -116,7 +116,7 @@ export class TrackerComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|   hasEffectiveFeeRate: boolean; | ||||
|   accelerateCtaType: 'alert' | 'button' = 'button'; | ||||
|   acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === ''; | ||||
|   acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR && this.stateService.network === ''; | ||||
|   accelerationEligible: boolean = false; | ||||
|   showAccelerationSummary = false; | ||||
|   accelerationFlowCompleted = false; | ||||
| @ -149,18 +149,12 @@ export class TrackerComponent implements OnInit, OnDestroy { | ||||
|   ngOnInit() { | ||||
|     this.onResize(); | ||||
| 
 | ||||
|     window['setStage'] = ((stage: TrackerStage) => { | ||||
|       this.zone.run(() => { | ||||
|         this.trackerStage = stage; | ||||
|         this.cd.markForCheck(); | ||||
|       }); | ||||
|     }).bind(this); | ||||
| 
 | ||||
|     this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === ''; | ||||
| 
 | ||||
|     if (this.acceleratorAvailable && this.stateService.referrer === 'https://cash.app/') { | ||||
|       this.paymentType = 'cashapp'; | ||||
|     } | ||||
|     this.miningService.getMiningStats('1w').subscribe(stats => { | ||||
|       this.miningStats = stats; | ||||
|     }); | ||||
| 
 | ||||
|     const urlParams = new URLSearchParams(window.location.search); | ||||
|     if (urlParams.get('cash_request_id')) { | ||||
|       this.showAccelerationSummary = true; | ||||
| @ -365,6 +359,7 @@ export class TrackerComponent implements OnInit, OnDestroy { | ||||
|     this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => { | ||||
|       this.now = Date.now(); | ||||
|       if (txPosition && txPosition.txid === this.txId && txPosition.position) { | ||||
|         this.loadingPosition = false; | ||||
|         this.mempoolPosition = txPosition.position; | ||||
|         this.accelerationPositions = txPosition.accelerationPositions; | ||||
|         if (this.tx && !this.tx.status.confirmed) { | ||||
| @ -390,11 +385,21 @@ export class TrackerComponent implements OnInit, OnDestroy { | ||||
|             this.trackerStage = 'replaced'; | ||||
|           } | ||||
| 
 | ||||
|           if (txPosition.position?.block > 0 && this.tx.weight < 4000) { | ||||
|             this.accelerationEligible = true; | ||||
|             if (this.acceleratorAvailable && this.paymentType === 'cashapp') { | ||||
|           if (!this.mempoolPosition.accelerated) { | ||||
|             if (!this.accelerationFlowCompleted && !this.showAccelerationSummary && this.mempoolPosition.block > 0) { | ||||
|               this.showAccelerationSummary = true; | ||||
|               this.miningService.getMiningStats('1w').subscribe(stats => { | ||||
|                 this.miningStats = stats; | ||||
|               }); | ||||
|             } | ||||
|             if (txPosition.position?.block > 0) { | ||||
|               this.accelerationEligible = true; | ||||
|             } | ||||
|           } else if (this.showAccelerationSummary) { | ||||
|             setTimeout(() => { | ||||
|               this.accelerationFlowCompleted = true; | ||||
|               this.showAccelerationSummary = false; | ||||
|             }, 2000); | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
| @ -449,6 +454,7 @@ export class TrackerComponent implements OnInit, OnDestroy { | ||||
|       )) | ||||
|       .subscribe((tx: Transaction) => { | ||||
|           if (!tx) { | ||||
|             this.loadingPosition = false; | ||||
|             this.fetchCachedTx$.next(this.txId); | ||||
|             this.seoService.logSoft404(); | ||||
|             return; | ||||
| @ -481,6 +487,7 @@ export class TrackerComponent implements OnInit, OnDestroy { | ||||
|             } | ||||
|           } else { | ||||
|             this.trackerStage = 'confirmed'; | ||||
|             this.loadingPosition = false; | ||||
|             this.fetchAcceleration$.next(tx.status.block_hash); | ||||
|             this.fetchMiningInfo$.next({ hash: tx.status.block_hash, height: tx.status.block_height, txid: tx.txid }); | ||||
|             this.transactionTime = 0; | ||||
| @ -736,17 +743,23 @@ export class TrackerComponent implements OnInit, OnDestroy { | ||||
|       return; | ||||
|     } | ||||
|     this.enterpriseService.goal(8); | ||||
|     this.accelerationFlowCompleted = false; | ||||
|     this.showAccelerationSummary = true && this.acceleratorAvailable; | ||||
|     this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview; | ||||
|     this.scrollIntoAccelPreview = true; | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   get isLoading(): boolean { | ||||
|     return this.isLoadingTx || this.loadingCachedTx || this.loadingPosition; | ||||
|   } | ||||
| 
 | ||||
|   resetTransaction() { | ||||
|     this.error = undefined; | ||||
|     this.tx = null; | ||||
|     this.txChanged$.next(true); | ||||
|     this.waitingForTransaction = false; | ||||
|     this.isLoadingTx = true; | ||||
|     this.loadingPosition = true; | ||||
|     this.rbfTransaction = undefined; | ||||
|     this.replaced = false; | ||||
|     this.latestReplacement = ''; | ||||
|  | ||||
							
								
								
									
										51
									
								
								frontend/src/app/components/tracker/tracker.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								frontend/src/app/components/tracker/tracker.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { Routes, RouterModule } from '@angular/router'; | ||||
| import { SharedModule } from '../../shared/shared.module'; | ||||
| import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module'; | ||||
| import { GraphsModule } from '../../graphs/graphs.module'; | ||||
| import { TrackerComponent } from '../tracker/tracker.component'; | ||||
| import { TrackerBarComponent } from '../tracker/tracker-bar.component'; | ||||
| import { TransactionModule } from '../transaction/transaction.module'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|   { | ||||
|     path: ':id', | ||||
|     component: TrackerComponent, | ||||
|     data: { | ||||
|       ogImage: true | ||||
|     } | ||||
|   } | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|   imports: [ | ||||
|     RouterModule.forChild(routes) | ||||
|   ], | ||||
|   exports: [ | ||||
|     RouterModule | ||||
|   ] | ||||
| }) | ||||
| export class TrackerRoutingModule { } | ||||
| 
 | ||||
| @NgModule({ | ||||
|   imports: [ | ||||
|     CommonModule, | ||||
|     TrackerRoutingModule, | ||||
|     TransactionModule, | ||||
|     SharedModule, | ||||
|     GraphsModule, | ||||
|     TxBowtieModule, | ||||
|   ], | ||||
|   declarations: [ | ||||
|     TrackerComponent, | ||||
|     TrackerBarComponent, | ||||
|   ] | ||||
| }) | ||||
| export class TrackerModule { } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @ -80,11 +80,27 @@ | ||||
|       <div class="title float-left"> | ||||
|         <h2 i18n="transaction.accelerate|Accelerate button label">Accelerate</h2> | ||||
|       </div> | ||||
| 
 | ||||
|       <button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="closeAccelerator()" i18n="hide-diagram">Hide accelerator</button> | ||||
|       <button *ngIf="hasAccelerationDetails" class="btn btn-sm btn-outline-info float-right ml-2" (click)="showAccelerationDetails = !showAccelerationDetails">Details</button> | ||||
| 
 | ||||
|       <div class="clearfix"></div> | ||||
| 
 | ||||
|       <div class="box"> | ||||
|         <app-accelerate-preview [tx]="tx" [miningStats]="miningStats" [mempoolPosition]="mempoolPosition" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview> | ||||
|       </div> | ||||
|       <ng-container *ngIf="(ETA$ | async) as eta;"> | ||||
|         <app-accelerate-checkout | ||||
|           *ngIf="(da$ | async) as da;" | ||||
|           [cashappEnabled]="accelerationEligible" | ||||
|           [advancedEnabled]="true" | ||||
|           [tx]="tx" | ||||
|           [eta]="eta" | ||||
|           [miningStats]="miningStats" | ||||
|           [scrollEvent]="scrollIntoAccelPreview" | ||||
|           [showDetails]="showAccelerationDetails" | ||||
|           [noCTA]="true" | ||||
|           (hasDetails)="setHasAccelerationDetails($event)" | ||||
|           class="h-100 w-100" | ||||
|         ></app-accelerate-checkout> | ||||
|       </ng-container> | ||||
|     </ng-container> | ||||
| 
 | ||||
|     <ng-template [ngIf]="showCpfpDetails"> | ||||
| @ -535,21 +551,23 @@ | ||||
|         <td> | ||||
|           <ng-container *ngIf="(ETA$ | async) as eta; else etaSkeleton"> | ||||
|             @if (eta.blocks >= 7) { | ||||
|               <span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''"> | ||||
|               <span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''"> | ||||
|                 <span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span> | ||||
|                 @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') { | ||||
|                 @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) { | ||||
|                   <a class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a> | ||||
|                 } | ||||
|               </span> | ||||
|             } @else if (network === 'liquid' || network === 'liquidtestnet') { | ||||
|               <app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time> | ||||
|             } @else { | ||||
|               <span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''"> | ||||
|               <span [class]="(!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''"> | ||||
|                 <app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time> | ||||
|                 @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') { | ||||
|                   <a class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a> | ||||
|                 @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) { | ||||
|                   <a class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a> | ||||
|                 } | ||||
|               </span> | ||||
|               <span class="eta justify-content-end"> | ||||
|               </span> | ||||
|             } | ||||
|           </ng-container> | ||||
|           <ng-template #etaSkeleton> | ||||
| @ -648,7 +666,7 @@ | ||||
| <ng-template #acceleratingRow> | ||||
|   <tr> | ||||
|     <td rowspan="2" colspan="2" style="padding: 0;"> | ||||
|       <app-active-acceleration-box [tx]="tx" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats"></app-active-acceleration-box> | ||||
|       <app-active-acceleration-box [tx]="tx" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [chartPositionLeft]="isMobile"></app-active-acceleration-box> | ||||
|     </td> | ||||
|   </tr> | ||||
|   <tr></tr> | ||||
|  | ||||
| @ -300,7 +300,6 @@ | ||||
| 
 | ||||
| .accelerateDeepMempool { | ||||
|   align-self: auto; | ||||
|   margin-top: 3px; | ||||
|   margin-left: auto; | ||||
|   background-color: var(--tertiary); | ||||
|   @media (max-width: 995px) { | ||||
|  | ||||
| @ -136,9 +136,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|   taprootEnabled: boolean; | ||||
|   hasEffectiveFeeRate: boolean; | ||||
|   accelerateCtaType: 'alert' | 'button' = 'button'; | ||||
|   acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === ''; | ||||
|   acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR && this.stateService.network === ''; | ||||
|   showAccelerationSummary = false; | ||||
|   showAccelerationDetails = false; | ||||
|   hasAccelerationDetails = false; | ||||
|   accelerationFlowCompleted = false; | ||||
|   scrollIntoAccelPreview = false; | ||||
|   accelerationEligible = false; | ||||
|   auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true; | ||||
| 
 | ||||
|   @ViewChild('graphContainer') | ||||
| @ -166,15 +170,24 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|   ) {} | ||||
| 
 | ||||
|   ngOnInit() { | ||||
|     this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === ''; | ||||
| 
 | ||||
|     this.enterpriseService.page(); | ||||
| 
 | ||||
|     const urlParams = new URLSearchParams(window.location.search); | ||||
|     if (urlParams.get('cash_request_id')) { | ||||
|       this.showAccelerationSummary = true; | ||||
|     } | ||||
| 
 | ||||
|     if (!this.stateService.isLiquid) { | ||||
|       this.miningService.getMiningStats('1w').subscribe(stats => { | ||||
|         this.miningStats = stats; | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     this.websocketService.want(['blocks', 'mempool-blocks']); | ||||
|     this.stateService.networkChanged$.subscribe( | ||||
|       (network) => { | ||||
|         this.network = network; | ||||
|         this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === ''; | ||||
|         this.acceleratorAvailable = this.stateService.env.ACCELERATOR && this.stateService.network === ''; | ||||
|       } | ||||
|     ); | ||||
| 
 | ||||
| @ -398,6 +411,24 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|           } else if ((this.tx?.acceleration && txPosition.position.acceleratedBy)) { | ||||
|             this.tx.acceleratedBy = txPosition.position.acceleratedBy; | ||||
|           } | ||||
| 
 | ||||
|           if (this.stateService.network === '') { | ||||
|             if (!this.mempoolPosition.accelerated) { | ||||
|               if (!this.accelerationFlowCompleted && !this.showAccelerationSummary) { | ||||
|                 this.showAccelerationSummary = true; | ||||
|                 this.miningService.getMiningStats('1w').subscribe(stats => { | ||||
|                   this.miningStats = stats; | ||||
|                 }); | ||||
|               } | ||||
|               if (txPosition.position?.block > 0 && this.tx.weight < 4000) { | ||||
|                 this.accelerationEligible = true; | ||||
|               } | ||||
|             } else if (this.showAccelerationSummary) { | ||||
|               setTimeout(() => { | ||||
|                 this.closeAccelerator(); | ||||
|               }, 2000); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         this.mempoolPosition = null; | ||||
| @ -682,14 +713,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.miningService.getMiningStats('1w').subscribe(stats => { | ||||
|       this.miningStats = stats; | ||||
|     }); | ||||
| 
 | ||||
|     document.location.hash = '#accelerate'; | ||||
|     this.enterpriseService.goal(8); | ||||
|     this.showAccelerationSummary = true && this.acceleratorAvailable; | ||||
|     this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview; | ||||
|     this.accelerationFlowCompleted = false; | ||||
|     this.showAccelerationSummary = this.acceleratorAvailable; | ||||
|     this.scrollIntoAccelPreview = true; | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
| @ -748,6 +776,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|       this.tx.acceleratedBy = cpfpInfo.acceleratedBy; | ||||
|       this.setIsAccelerated(firstCpfp); | ||||
|     } | ||||
| 
 | ||||
|     if (!this.isAcceleration && this.fragmentParams.has('accelerate')) { | ||||
|       this.onAccelerateClicked(); | ||||
|     } | ||||
| 
 | ||||
|     this.txChanged$.next(true); | ||||
| 
 | ||||
|     this.cpfpInfo = cpfpInfo; | ||||
| @ -761,8 +794,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
| 
 | ||||
|   setIsAccelerated(initialState: boolean = false) { | ||||
|     this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id)))); | ||||
|     if (this.isAcceleration && initialState) { | ||||
|       this.showAccelerationSummary = false; | ||||
|     if (this.isAcceleration) { | ||||
|       if (initialState) { | ||||
|         this.accelerationFlowCompleted = true; | ||||
|         this.showAccelerationSummary = false; | ||||
|       } else if (this.showAccelerationSummary) { | ||||
|         setTimeout(() => { | ||||
|           this.closeAccelerator(); | ||||
|         }, 2000); | ||||
|       } | ||||
|     } | ||||
|     if (this.isAcceleration) { | ||||
|       // this immediately returns cached stats if we fetched them recently
 | ||||
| @ -831,7 +871,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|     this.rbfReplaces = []; | ||||
|     this.filters = []; | ||||
|     this.showCpfpDetails = false; | ||||
|     this.showAccelerationDetails = false; | ||||
|     this.accelerationInfo = null; | ||||
|     this.accelerationEligible = false; | ||||
|     this.txInBlockIndex = null; | ||||
|     this.mempoolPosition = null; | ||||
|     this.pool = null; | ||||
| @ -848,6 +890,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|     this.stateService.markBlock$.next({}); | ||||
|   } | ||||
| 
 | ||||
|   closeAccelerator(): void { | ||||
|     this.accelerationFlowCompleted = true; | ||||
|     this.showAccelerationSummary = false; | ||||
|   } | ||||
| 
 | ||||
|   roundToOneDecimal(cpfpTx: any): number { | ||||
|     return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1); | ||||
|   } | ||||
| @ -885,18 +932,18 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|   // simulate normal anchor fragment behavior
 | ||||
|   applyFragment(): void { | ||||
|     const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === ''); | ||||
|     if (anchor?.length) { | ||||
|       if (anchor[0] === 'accelerate') { | ||||
|         setTimeout(this.onAccelerateClicked.bind(this), 100); | ||||
|       } else { | ||||
|         const anchorElement = document.getElementById(anchor[0]); | ||||
|         if (anchorElement) { | ||||
|           anchorElement.scrollIntoView(); | ||||
|         } | ||||
|     if (anchor?.length && anchor[0] !== 'accelerate') { | ||||
|       const anchorElement = document.getElementById(anchor[0]); | ||||
|       if (anchorElement) { | ||||
|         anchorElement.scrollIntoView(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setHasAccelerationDetails(hasDetails: boolean): void { | ||||
|     this.hasAccelerationDetails = hasDetails; | ||||
|   } | ||||
| 
 | ||||
|   @HostListener('window:resize', ['$event']) | ||||
|   setGraphSize(): void { | ||||
|     this.isMobile = window.innerWidth < 850; | ||||
| @ -911,6 +958,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   isLoggedIn(): boolean { | ||||
|     const auth = this.storageService.getAuth(); | ||||
|     return auth !== null; | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     this.subscription.unsubscribe(); | ||||
|     this.fetchCpfpSubscription.unsubscribe(); | ||||
|  | ||||
| @ -5,10 +5,15 @@ import { TransactionComponent } from './transaction.component'; | ||||
| import { SharedModule } from '../../shared/shared.module'; | ||||
| import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module'; | ||||
| import { GraphsModule } from '../../graphs/graphs.module'; | ||||
| import { AcceleratePreviewComponent } from '../accelerate-preview/accelerate-preview.component'; | ||||
| import { AccelerateFeeGraphComponent } from '../accelerate-preview/accelerate-fee-graph.component'; | ||||
| import { AccelerateCheckout } from '../accelerate-checkout/accelerate-checkout.component'; | ||||
| import { AccelerateFeeGraphComponent } from '../accelerate-checkout/accelerate-fee-graph.component'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|   { | ||||
|     path: '', | ||||
|     redirectTo: '/', | ||||
|     pathMatch: 'full', | ||||
|   }, | ||||
|   { | ||||
|     path: ':id', | ||||
|     component: TransactionComponent, | ||||
| @ -38,7 +43,12 @@ export class TransactionRoutingModule { } | ||||
|   ], | ||||
|   declarations: [ | ||||
|     TransactionComponent, | ||||
|     AcceleratePreviewComponent, | ||||
|     AccelerateCheckout, | ||||
|     AccelerateFeeGraphComponent, | ||||
|   ], | ||||
|   exports: [ | ||||
|     TransactionComponent, | ||||
|     AccelerateCheckout, | ||||
|     AccelerateFeeGraphComponent, | ||||
|   ] | ||||
| }) | ||||
|  | ||||
| @ -72,7 +72,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit { | ||||
|     this.auditEnabled = this.env.AUDIT; | ||||
|     this.network$ = merge(of(''), this.stateService.networkChanged$).pipe( | ||||
|       tap((network: string) => { | ||||
|         if (this.env.BASE_MODULE === 'mempool' && network !== '') { | ||||
|         if (this.env.BASE_MODULE === 'mempool' && network !== '' && this.env.ROOT_NETWORK === '') { | ||||
|           this.baseNetworkUrl = `/${network}`; | ||||
|         } else if (this.env.BASE_MODULE === 'liquid') { | ||||
|           if (!['', 'liquid'].includes(network)) { | ||||
| @ -195,6 +195,10 @@ export class ApiDocsComponent implements OnInit, AfterViewInit { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (network === this.env.ROOT_NETWORK) { | ||||
|       curlNetwork = ''; | ||||
|     } | ||||
| 
 | ||||
|     let text = code.codeTemplate.curl; | ||||
|     for (let index = 0; index < curlResponse.length; index++) { | ||||
|       const curlText = curlResponse[index]; | ||||
|  | ||||
| @ -284,7 +284,7 @@ yarn add @mempool/liquid.js`; | ||||
|     const headersString = code.headers ? ` -H "${code.headers}"` : ``; | ||||
|      | ||||
|     if (this.env.BASE_MODULE === 'mempool') { | ||||
|       if (this.network === 'main' || this.network === '') { | ||||
|       if (this.network === 'main' || this.network === '' || this.network === this.env.ROOT_NETWORK) { | ||||
|         if (this.method === 'POST') { | ||||
|           return `curl${headersString} -X POST -sSLd "${text}"`; | ||||
|         } | ||||
| @ -296,7 +296,7 @@ yarn add @mempool/liquid.js`; | ||||
|       return `curl${headersString} -sSL "${this.hostname}/${this.network}${text}"`; | ||||
|     } else if (this.env.BASE_MODULE === 'liquid') { | ||||
|       if (this.method === 'POST') { | ||||
|         if (this.network !== 'liquid') { | ||||
|         if (this.network !== 'liquid' || this.network === this.env.ROOT_NETWORK) { | ||||
|           text = text.replace('/api', `/${this.network}/api`); | ||||
|         } | ||||
|         return `curl${headersString} -X POST -sSLd "${text}"`; | ||||
|  | ||||
| @ -60,10 +60,14 @@ const routes: Routes = [ | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         path: 'acceleration/list', | ||||
|         path: 'acceleration/list/:page', | ||||
|         data: { networks: ['bitcoin'] }, | ||||
|         component: AccelerationsListComponent, | ||||
|       }, | ||||
|       { | ||||
|         path: 'acceleration/list', | ||||
|         redirectTo: 'acceleration/list/1', | ||||
|       }, | ||||
|       { | ||||
|         path: 'mempool-block/:id', | ||||
|         data: { networks: ['bitcoin', 'liquid'] }, | ||||
|  | ||||
| @ -84,10 +84,14 @@ const routes: Routes = [ | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         path: 'audit/pegs', | ||||
|         path: 'audit/pegs/:page', | ||||
|         data: { networks: ['liquid'] }, | ||||
|         component: RecentPegsListComponent, | ||||
|       }, | ||||
|       { | ||||
|         path: 'audit/pegs', | ||||
|         redirectTo: 'audit/pegs/1' | ||||
|       }, | ||||
|       { | ||||
|         path: 'assets', | ||||
|         data: { networks: ['liquid'] }, | ||||
|  | ||||
| @ -45,9 +45,13 @@ const routes: Routes = [ | ||||
|         loadChildren: () => import('./components/about/about.module').then(m => m.AboutModule), | ||||
|       }, | ||||
|       { | ||||
|         path: 'blocks', | ||||
|         path: 'blocks/:page', | ||||
|         component: BlocksList, | ||||
|       }, | ||||
|       { | ||||
|         path: 'blocks', | ||||
|         redirectTo: 'blocks/1', | ||||
|       }, | ||||
|       { | ||||
|         path: 'rbf', | ||||
|         component: RbfList, | ||||
|  | ||||
| @ -3,8 +3,10 @@ import { AccelerationPosition, CpfpInfo, DifficultyAdjustment, MempoolPosition, | ||||
| import { StateService } from './state.service'; | ||||
| import { MempoolBlock } from '../interfaces/websocket.interface'; | ||||
| import { Transaction } from '../interfaces/electrs.interface'; | ||||
| import { MiningStats } from './mining.service'; | ||||
| import { MiningService, MiningStats } from './mining.service'; | ||||
| import { getUnacceleratedFeeRate } from '../shared/transaction.utils'; | ||||
| import { AccelerationEstimate } from '../components/accelerate-checkout/accelerate-checkout.component'; | ||||
| import { Observable, combineLatest, map, of, share, shareReplay, tap } from 'rxjs'; | ||||
| 
 | ||||
| export interface ETA { | ||||
|   now: number, // time at which calculation performed
 | ||||
| @ -19,8 +21,51 @@ export interface ETA { | ||||
| export class EtaService { | ||||
|   constructor( | ||||
|     private stateService: StateService, | ||||
|     private miningService: MiningService, | ||||
|   ) { } | ||||
| 
 | ||||
|   getProjectedEtaObservable(estimate: AccelerationEstimate, miningStats?: MiningStats): Observable<{ hashratePercentage: number, ETA: number, acceleratedETA: number }> { | ||||
|     return combineLatest([ | ||||
|       this.stateService.mempoolTxPosition$.pipe(map(p => p?.position)), | ||||
|       this.stateService.difficultyAdjustment$, | ||||
|       miningStats ? of(miningStats) : this.miningService.getMiningStats('1w'), | ||||
|     ]).pipe( | ||||
|       map(([mempoolPosition, da, miningStats]) => { | ||||
|         if (!mempoolPosition || !estimate?.pools?.length || !miningStats || !da) { | ||||
|           return { | ||||
|             hashratePercentage: undefined, | ||||
|             ETA: undefined, | ||||
|             acceleratedETA: undefined, | ||||
|           }; | ||||
|         } | ||||
|         const pools: { [id: number]: SinglePoolStats } = {}; | ||||
|         for (const pool of miningStats.pools) { | ||||
|           pools[pool.poolUniqueId] = pool; | ||||
|         } | ||||
| 
 | ||||
|         let totalAcceleratedHashrate = 0; | ||||
|         for (const poolId of estimate.pools) { | ||||
|           const pool = pools[poolId]; | ||||
|           if (!pool) { | ||||
|             continue; | ||||
|           } | ||||
|           totalAcceleratedHashrate += pool.lastEstimatedHashrate; | ||||
|         } | ||||
|         const acceleratingHashrateFraction = (totalAcceleratedHashrate / miningStats.lastEstimatedHashrate); | ||||
| 
 | ||||
|         return { | ||||
|           hashratePercentage: acceleratingHashrateFraction * 100, | ||||
|           ETA: Date.now() + da.timeAvg * mempoolPosition.block, | ||||
|           acceleratedETA: this.calculateETAFromShares([ | ||||
|             { block: mempoolPosition.block, hashrateShare: (1 - acceleratingHashrateFraction) }, | ||||
|             { block: 0, hashrateShare: acceleratingHashrateFraction }, | ||||
|           ], da).time, | ||||
|         }; | ||||
|       }), | ||||
|       shareReplay() | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   mempoolPositionFromFees(feerate: number, mempoolBlocks: MempoolBlock[]): MempoolPosition { | ||||
|     for (let txInBlockIndex = 0; txInBlockIndex < mempoolBlocks.length; txInBlockIndex++) { | ||||
|       const block = mempoolBlocks[txInBlockIndex]; | ||||
| @ -41,7 +86,7 @@ export class EtaService { | ||||
|           return { | ||||
|             block: txInBlockIndex, | ||||
|             vsize: (1 - feePosition) * blockedFilledPercentage * this.stateService.blockVSize, | ||||
|           } | ||||
|           }; | ||||
|         } | ||||
|       } | ||||
|       if (feerate >= block.feeRange[block.feeRange.length - 1]) { | ||||
| @ -49,14 +94,14 @@ export class EtaService { | ||||
|         return { | ||||
|           block: txInBlockIndex, | ||||
|           vsize: 0, | ||||
|         } | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|     // at the very back of the last block
 | ||||
|     return { | ||||
|       block: mempoolBlocks.length - 1, | ||||
|       vsize: mempoolBlocks[mempoolBlocks.length - 1].blockVSize, | ||||
|     } | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   calculateETA( | ||||
| @ -88,7 +133,7 @@ export class EtaService { | ||||
|         time: now + (60_000 * (mempoolPosition.block + 1)), | ||||
|         wait: (60_000 * (mempoolPosition.block + 1)), | ||||
|         blocks: mempoolPosition.block + 1, | ||||
|       } | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     // difficulty adjustment estimate is required to know avg block time on non-Liquid networks
 | ||||
| @ -104,7 +149,7 @@ export class EtaService { | ||||
|         time: wait + now + da.timeOffset, | ||||
|         wait, | ||||
|         blocks, | ||||
|       } | ||||
|       }; | ||||
|     } else { | ||||
|       // accelerated transactions
 | ||||
| 
 | ||||
| @ -121,7 +166,7 @@ export class EtaService { | ||||
|         pools[pool.poolUniqueId] = pool; | ||||
|       } | ||||
|       const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks); | ||||
|       let totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId].lastEstimatedHashrate), 0); | ||||
|       const totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId].lastEstimatedHashrate), 0); | ||||
|       const shares = [ | ||||
|         { | ||||
|           block: unacceleratedPosition.block, | ||||
| @ -163,7 +208,7 @@ export class EtaService { | ||||
|         // find H_i
 | ||||
|         const H = shares.reduce((total, share) => total + (share.block <= i ? share.hashrateShare : 0), 0); | ||||
|         // find S_i
 | ||||
|         let S = H * (1 - tailProb); | ||||
|         const S = H * (1 - tailProb); | ||||
|         // accumulate sum (S_i x i)
 | ||||
|         Q += (S * (i + 1)); | ||||
|         // accumulate sum (S_j)
 | ||||
| @ -178,6 +223,6 @@ export class EtaService { | ||||
|         time: eta + now + da.timeOffset, | ||||
|         wait: eta, | ||||
|         blocks: Math.ceil(eta / da.adjustedTimeAvg), | ||||
|       } | ||||
|       }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -167,4 +167,20 @@ export class ServicesApiServices { | ||||
|   requestTestnet4Coins$(address: string, sats: number) { | ||||
|     return this.httpClient.get<{txid: string}>(`${SERVICES_API_PREFIX}/testnet4/faucet/request?address=${address}&sats=${sats}`, { responseType: 'json' }); | ||||
|   } | ||||
| 
 | ||||
|   generateBTCPayAcceleratorInvoice$(txid: string, sats: number): Observable<any> { | ||||
|     const params = { | ||||
|       product: txid, | ||||
|       amount: sats, | ||||
|     }; | ||||
|     return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/payments/bitcoin`, params); | ||||
|   } | ||||
| 
 | ||||
|   retreiveInvoice$(invoiceId: string): Observable<any[]> { | ||||
|     return this.httpClient.get<any[]>(`${SERVICES_API_PREFIX}/payments/bitcoin/invoice?id=${invoiceId}`); | ||||
|   } | ||||
| 
 | ||||
|   getPaymentStatus$(orderId: string): Observable<any[]> { | ||||
|     return this.httpClient.get<any[]>(`${SERVICES_API_PREFIX}/payments/bitcoin/check?order_id=${orderId}`); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -150,7 +150,7 @@ export class StateService { | ||||
|   utxoSpent$ = new Subject<object>(); | ||||
|   difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1); | ||||
|   mempoolTransactions$ = new Subject<Transaction>(); | ||||
|   mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null, accelerationPositions?: AccelerationPosition[] }>(); | ||||
|   mempoolTxPosition$ = new BehaviorSubject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null, accelerationPositions?: AccelerationPosition[] }>(null); | ||||
|   mempoolRemovedTransactions$ = new Subject<Transaction>(); | ||||
|   multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>(); | ||||
|   blockTransactions$ = new Subject<Transaction>(); | ||||
|  | ||||
| @ -29,6 +29,7 @@ const MempoolErrors = { | ||||
|   'faucet_address_not_allowed': `You cannot use this address`, | ||||
|   'faucet_below_minimum': `Requested amount is too small`, | ||||
|   'faucet_above_maximum': `Requested amount is too high`, | ||||
|   'payment_method_not_allowed': `You are not allowed to use this payment method`, | ||||
| } as { [error: string]: string }; | ||||
| 
 | ||||
| export function isMempoolError(error: string) { | ||||
|  | ||||
| @ -50,8 +50,6 @@ import { BlockOverviewGraphComponent } from '../components/block-overview-graph/ | ||||
| import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component'; | ||||
| import { BlockFiltersComponent } from '../components/block-filters/block-filters.component'; | ||||
| import { AddressGroupComponent } from '../components/address-group/address-group.component'; | ||||
| import { TrackerComponent } from '../components/tracker/tracker.component'; | ||||
| import { TrackerBarComponent } from '../components/tracker/tracker-bar.component'; | ||||
| import { SearchFormComponent } from '../components/search-form/search-form.component'; | ||||
| import { AddressLabelsComponent } from '../components/address-labels/address-labels.component'; | ||||
| import { FooterComponent } from '../components/footer/footer.component'; | ||||
| @ -100,7 +98,6 @@ import { MempoolErrorComponent } from './components/mempool-error/mempool-error. | ||||
| import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component'; | ||||
| import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component'; | ||||
| import { AccelerationStatsComponent } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; | ||||
| import { AccelerateCheckout } from '../components/accelerate-checkout/accelerate-checkout.component'; | ||||
| 
 | ||||
| import { BlockViewComponent } from '../components/block-view/block-view.component'; | ||||
| import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component'; | ||||
| @ -115,6 +112,7 @@ import { HttpErrorComponent } from '../shared/components/http-error/http-error.c | ||||
| import { TwitterWidgetComponent } from '../components/twitter-widget/twitter-widget.component'; | ||||
| import { FaucetComponent } from '../components/faucet/faucet.component'; | ||||
| import { TwitterLogin } from '../components/twitter-login/twitter-login.component'; | ||||
| import { BitcoinInvoiceComponent } from '../components/bitcoin-invoice/bitcoin-invoice.component'; | ||||
| 
 | ||||
| import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-directives/weight-directives'; | ||||
| 
 | ||||
| @ -164,8 +162,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     BlockFiltersComponent, | ||||
|     TransactionsListComponent, | ||||
|     AddressGroupComponent, | ||||
|     TrackerComponent, | ||||
|     TrackerBarComponent, | ||||
|     SearchFormComponent, | ||||
|     AddressLabelsComponent, | ||||
|     FooterComponent, | ||||
| @ -224,12 +220,12 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     MempoolErrorComponent, | ||||
|     AccelerationsListComponent, | ||||
|     AccelerationStatsComponent, | ||||
|     AccelerateCheckout, | ||||
|     PendingStatsComponent, | ||||
|     HttpErrorComponent, | ||||
|     TwitterWidgetComponent, | ||||
|     FaucetComponent, | ||||
|     TwitterLogin, | ||||
|     BitcoinInvoiceComponent, | ||||
|   ], | ||||
|   imports: [ | ||||
|     CommonModule, | ||||
| @ -305,8 +301,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     BlockFiltersComponent, | ||||
|     TransactionsListComponent, | ||||
|     AddressGroupComponent, | ||||
|     TrackerComponent, | ||||
|     TrackerBarComponent, | ||||
|     SearchFormComponent, | ||||
|     AddressLabelsComponent, | ||||
|     FooterComponent, | ||||
| @ -354,11 +348,11 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     MempoolErrorComponent, | ||||
|     AccelerationsListComponent, | ||||
|     AccelerationStatsComponent, | ||||
|     AccelerateCheckout, | ||||
|     PendingStatsComponent, | ||||
|     HttpErrorComponent, | ||||
|     TwitterWidgetComponent, | ||||
|     TwitterLogin, | ||||
|     BitcoinInvoiceComponent, | ||||
| 
 | ||||
|     MempoolBlockOverviewComponent, | ||||
|     ClockchainComponent, | ||||
|  | ||||
| @ -181,7 +181,7 @@ export function isNonStandard(tx: Transaction): boolean { | ||||
|       dustSize += getVarIntLength(dustSize); | ||||
|       // add value size
 | ||||
|       dustSize += 8; | ||||
|       if (['v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) { | ||||
|       if (isWitnessProgram(vout.scriptpubkey)) { | ||||
|         dustSize += 67; | ||||
|       } else { | ||||
|         dustSize += 148; | ||||
| @ -335,19 +335,21 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac | ||||
|       case 'v0_p2wpkh': flags |= TransactionFlags.p2wpkh; break; | ||||
|       case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break; | ||||
|       case 'v1_p2tr': { | ||||
|         if (!vin.witness?.length) { | ||||
|           throw new Error('Taproot input missing witness data'); | ||||
|         } | ||||
|         flags |= TransactionFlags.p2tr; | ||||
|         // in taproot, if the last witness item begins with 0x50, it's an annex
 | ||||
|         const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50'); | ||||
|         // script spends have more than one witness item, not counting the annex (if present)
 | ||||
|         if (vin.witness.length > (hasAnnex ? 2 : 1)) { | ||||
|           // the script itself is the second-to-last witness item, not counting the annex
 | ||||
|           const asm = vin.inner_witnessscript_asm; | ||||
|           // inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
 | ||||
|           if (asm?.includes('OP_0 OP_IF')) { | ||||
|             flags |= TransactionFlags.inscription; | ||||
|         // every valid taproot input has at least one witness item, however transactions
 | ||||
|         // created before taproot activation don't need to have any witness data
 | ||||
|         // (see https://mempool.space/tx/b10c007c60e14f9d087e0291d4d0c7869697c6681d979c6639dbd960792b4d41)
 | ||||
|         if (vin.witness?.length) { | ||||
|           // in taproot, if the last witness item begins with 0x50, it's an annex
 | ||||
|           const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50'); | ||||
|           // script spends have more than one witness item, not counting the annex (if present)
 | ||||
|           if (vin.witness.length > (hasAnnex ? 2 : 1)) { | ||||
|             // the script itself is the second-to-last witness item, not counting the annex
 | ||||
|             const asm = vin.inner_witnessscript_asm; | ||||
|             // inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
 | ||||
|             if (asm?.includes('OP_0 OP_IF')) { | ||||
|               flags |= TransactionFlags.inscription; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } break; | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								frontend/src/resources/bitcoin-logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/resources/bitcoin-logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 58 KiB | 
							
								
								
									
										1
									
								
								frontend/src/resources/btcpay.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/resources/btcpay.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <svg id="レイヤー_1" data-name="レイヤー 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 236.18 102.3"><defs><style>.cls-1{fill:#cedc21;}.cls-2{fill:#51b13e;}.cls-3{fill:#1e7a44;}.cls-4{fill:#fff;}</style></defs><title>btcpay3</title><path class="cls-1" d="M38.55,201.73a6,6,0,0,1-6-6V105.44a6,6,0,0,1,12,0v90.29A6,6,0,0,1,38.55,201.73Z" transform="translate(-32.55 -99.43)"/><path class="cls-2" d="M38.56,201.73A6,6,0,0,1,36,190.31l36.18-17.17L35,145.76a6,6,0,1,1,7.11-9.66l45.24,33.33a6,6,0,0,1-1,10.25L41.13,201.15A5.9,5.9,0,0,1,38.56,201.73Z" transform="translate(-32.55 -99.43)"/><path class="cls-1" d="M38.56,166.24A6,6,0,0,1,35,155.41L72.16,128,36,110.86A6,6,0,1,1,41.13,100l45.24,21.47a6,6,0,0,1,1,10.25L42.11,165.07A6,6,0,0,1,38.56,166.24Z" transform="translate(-32.55 -99.43)"/><polygon class="cls-3" points="12 38.46 12 63.84 29.21 51.16 12 38.46"/><rect class="cls-4" y="27.82" width="12" height="29.25"/><path class="cls-1" d="M44.55,105.44a6,6,0,0,0-12,0V181h12Z" transform="translate(-32.55 -99.43)"/><path class="cls-4" d="M123.92,149.86c3.84,1.09,6,4.57,6,8.93,0,6.81-4.15,10-9.82,10H107.14V132.4h11.43c5.56,0,9.87,2.65,9.87,9.56C128.44,145.44,127,148.66,123.92,149.86Zm-5.3-.89c4.1,0,7.43-1.45,7.43-7.06s-3.43-7.17-7.59-7.17h-8.88V149Zm1.3,17.41c4.15,0,7.48-2.18,7.48-7.59,0-5.82-3.79-7.58-8.52-7.58h-9.3v15.17Z" transform="translate(-32.55 -99.43)"/><path class="cls-4" d="M154.21,132.4v2.23h-10v34.14h-2.44V134.63h-10V132.4Z" transform="translate(-32.55 -99.43)"/><path class="cls-4" d="M171.3,131.88c6.19,0,11.8,3.12,13.57,10.55h-2.34c-1.66-6.08-6.55-8.32-11.28-8.32-8.57,0-13.09,7-13.09,16.47,0,10,4.52,16.37,13.14,16.37,5.1,0,9.67-2.29,11.44-9h2.33a13.6,13.6,0,0,1-13.77,11.33c-9.71,0-15.53-7.17-15.53-18.71C155.77,139.52,161.9,131.88,171.3,131.88Z" transform="translate(-32.55 -99.43)"/><path class="cls-4" d="M203.36,132.4c6.29,0,10.86,4.1,10.86,12,0,7.48-4.57,12-10.86,12H193.8v12.32h-2.39V132.4Zm0,21.66c4.63,0,8.42-3,8.42-9.66s-3.64-9.71-8.42-9.71H193.8v19.37Z" transform="translate(-32.55 -99.43)"/><path class="cls-4" d="M214.64,168.77v-.31l14.49-36.16h1.1l14.34,36.16v.31H242l-4.1-10.5H221.34l-4.1,10.5Zm15-32.16L222.17,156h14.91Z" transform="translate(-32.55 -99.43)"/><path class="cls-4" d="M266.13,132.4h2.6v.36L257.4,153.65v15.12h-2.49V153.65l-11.38-20.94v-.31h2.65l4.93,9.25,5,9.61h0l5-9.61Z" transform="translate(-32.55 -99.43)"/></svg> | ||||
| After Width: | Height: | Size: 2.3 KiB | 
							
								
								
									
										1
									
								
								frontend/src/resources/cash-app.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/resources/cash-app.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| After Width: | Height: | Size: 5.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								frontend/src/resources/lightning-logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/resources/lightning-logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 66 KiB | 
							
								
								
									
										
											BIN
										
									
								
								frontend/src/resources/profile/coldcard.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/resources/profile/coldcard.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										28
									
								
								unfurler/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										28
									
								
								unfurler/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -491,12 +491,12 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/braces": { | ||||
|       "version": "3.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", | ||||
|       "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", | ||||
|       "version": "3.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", | ||||
|       "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "fill-range": "^7.0.1" | ||||
|         "fill-range": "^7.1.1" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=8" | ||||
| @ -1252,9 +1252,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/fill-range": { | ||||
|       "version": "7.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", | ||||
|       "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", | ||||
|       "version": "7.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", | ||||
|       "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "to-regex-range": "^5.0.1" | ||||
| @ -3114,12 +3114,12 @@ | ||||
|       } | ||||
|     }, | ||||
|     "braces": { | ||||
|       "version": "3.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", | ||||
|       "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", | ||||
|       "version": "3.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", | ||||
|       "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "fill-range": "^7.0.1" | ||||
|         "fill-range": "^7.1.1" | ||||
|       } | ||||
|     }, | ||||
|     "buffer": { | ||||
| @ -3681,9 +3681,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "fill-range": { | ||||
|       "version": "7.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", | ||||
|       "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", | ||||
|       "version": "7.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", | ||||
|       "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "to-regex-range": "^5.0.1" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user