mirror of
https://github.com/mempool/mempool.git
synced 2024-11-19 09:52:14 +01:00
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…
Reference in New Issue
Block a user