diff --git a/backend/src/__tests__/api/test-data/transactions-random.json b/backend/src/__tests__/api/test-data/transactions-random.json index ceab6e3c6..4bc0b731c 100644 --- a/backend/src/__tests__/api/test-data/transactions-random.json +++ b/backend/src/__tests__/api/test-data/transactions-random.json @@ -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 + } } ] \ No newline at end of file diff --git a/backend/src/api/acceleration/acceleration.routes.ts b/backend/src/api/acceleration/acceleration.routes.ts index ae0c3f7a8..082d53330 100644 --- a/backend/src/api/acceleration/acceleration.routes.ts +++ b/backend/src/api/acceleration/acceleration.routes.ts @@ -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 { + 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(); \ No newline at end of file diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 9248338ad..3e1fe2108 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -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); } diff --git a/backend/src/api/bitcoin/esplora-api.interface.ts b/backend/src/api/bitcoin/esplora-api.interface.ts index 0a0960e46..6e6860a41 100644 --- a/backend/src/api/bitcoin/esplora-api.interface.ts +++ b/backend/src/api/bitcoin/esplora-api.interface.ts @@ -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; diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index fc7dac135..97db07027 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -706,7 +706,7 @@ class Blocks { } const coinbaseTx = await bitcoinApi.$getCoinbaseTx(hash); - const addresses = new Set(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a)); + const addresses = new Set(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[]); await blocksRepository.$saveCoinbaseAddresses(hash, [...addresses]); // Logging diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 04f380418..1d3b68541 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -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 { diff --git a/docker/frontend/entrypoint.sh b/docker/frontend/entrypoint.sh index 5e79b262f..5b072ba8d 100644 --- a/docker/frontend/entrypoint.sh +++ b/docker/frontend/entrypoint.sh @@ -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__ diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 4fd1d2013..8e996953d 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -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', diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index 9d9091842..92d3de7f3 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -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; diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index d1e52efa5..41c0ce47f 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -53,13 +53,26 @@ Spiral - - - - - - - + + + + + + + + + + + + @@ -259,22 +272,10 @@ Bisq - - - BlueWallet - - - - Muun - Electrum - - - Specter - Sparrow @@ -283,21 +284,37 @@ Phoenix - - - LNBits + + + COLDCARD - - - Mercury + + + ZEUS + + + + Mutiny Blixt - - - ZEUS + + + Nunchuk + + + + BlueWallet + + + + Boltz + + + + LNBits @@ -307,13 +324,9 @@ Schildbach - - - Nunchuk - - - - bitcoin-s + + + Specter @@ -323,13 +336,13 @@ Galoy - - - Boltz + + + Muun - - - Mutiny + + + bitcoin-s diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index aecff02fc..a360e180c 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -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; + } } } } diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html index 44b027ae2..a0f84e226 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.html @@ -1,77 +1,412 @@ -
+
+ +
+
+
+ Transaction has now been submitted to mining pools for acceleration. +
+
+
@if (error) { -
- -
- } - - @else if (step === 'cta') { - -
-
-

Accelerate your Bitcoin transaction?

+
+
+
+ } + @else if (step === 'quote') { +
+ + + -
-
-
-
- - + +
+
+
You are currently on the waitlist
+
+ + @if (showDetails) { +
Your transaction
+
+
+ + Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s) + + + + + + + + + + + + + + + + + + +
Virtual size
+ Size in vbytes of this transaction (including unconfirmed ancestors) +
In-band fees + {{ estimate.txSummary.effectiveFee | number : '1.0-0' }} sats +
+ Fees already paid by this transaction (including unconfirmed ancestors) +
+
+
+
+ } +
How much faster?
+
+
+ + Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. + This will reduce your expected waiting time until the first confirmation to + +
+
+ +
+
+
+
+
+
+
+ + + +
+
+
+
+
+ +
Summary
+
+
+ + + + + @if (isLoggedIn()) { + + + + + + + + + + + } + @else { + + + + + + + + + + + + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Next block market rate + {{ estimate.targetFeeRate | number : '1.0-0' }} + sat/vB
+ Estimated extra fee required + + {{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }} + + sats + +
Target rate + {{ maxRateOptions[selectFeeRateIndex].rate | number : '1.0-0' }} + sat/vB
+ Extra fee required + + {{ maxRateOptions[selectFeeRateIndex].fee | number }} + + sats + +
Mempool Accelerator™ fees
+ Accelerator Service Fee + + +{{ estimate.mempoolBaseFee | number }} + + sats + +
+ Transaction Size Surcharge + + +{{ estimate.vsizeFee | number }} + + sats + +
+ Estimated acceleration cost ~{{ estimate.targetFeeRate | number : '1.0-0' }} sat/vB + + + {{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }} + + + sats + +
+ @if (isLoggedIn()) { + Maximum acceleration cost + } @else { + Acceleration cost + } + + + {{ cost | number }} + + + sats + + + +
Available balance + {{ estimate.userBalance | number }} + + sats + + + +
+
+ +
+
+
+
+
+ +
+
+
+
-
-
-
- -
+ + +
+
+
+ } + @else if (step === 'summary') { + + + @if (!noCTA) { +
+
+

Accelerate your Bitcoin transaction?

+
+
+ } + +
+
You are currently on the waitlist for Mempool Accelerator™
+
+ + +
+
+
+
+ + + +
+
+
+ Your transaction will be prioritized by up to {{ etaInfo.hashratePercentage | number : '1.1-1' }}% of miners. + +
+ +
+ +
+ +
+
+
+
-
-
-
+ } @else { +
+
+

Payment to mempool.space for acceleration of txid {{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}

+
+
+ @if (canPayWithBitcoin) { +
+ @if (invoice) { +

Pay {{ ((invoice.btcDue * 100_000_000) || cost) | number }} sats

+ + } @else { +

Loading invoice...

+
+
+
+ } +
+ @if (canPayWithCashapp) { +
+

OR

+
+ } + } + @if (canPayWithCashapp) { +
+

Pay with

+ +
+ } +
+
+ } + + +
+
+
+
+
+
- - } - - @else if (step === 'checkout') { +
+ +
+
+
+ +
+
+ } @else if (step === 'cashapp') {
-
-

Confirm your payment

+
+

Confirm your payment

- Payment to mempool.space for acceleration of txid {{ txid.substr(0, 10) }}..{{ txid.substr(-10) }} + Payment to mempool.space for acceleration of txid {{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}
@@ -109,16 +444,14 @@
- Changed your mind? - +
} - @else if (step === 'processing') {
-

Confirm your payment

+

Confirming your payment

@@ -135,5 +468,38 @@
} - + @else if (step === 'paid') { +
+
+

Accelerating your transaction

+
+
+ +
+
+
+ Confirming your acceleration with our mining pool partners... +
+
+
+
+ }
+ + + + + + + @if (isLoggedIn() || canPayWithBitcoin || canPayWithCashapp) { + + } @else { + + } + \ No newline at end of file diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss index 315bdbbd2..5cfd153bd 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.scss @@ -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; +} \ No newline at end of file diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index bd9bc453a..8c0d35dd9 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -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(); + @Input() cashappEnabled: boolean = true; + @Input() advancedEnabled: boolean = false; + @Input() forceMobile: boolean = false; + @Input() showDetails: boolean = false; + @Input() noCTA: boolean = false; + @Output() hasDetails = new EventEmitter(); + @Output() changeMode = new EventEmitter(); 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; } } diff --git a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.html b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.html similarity index 100% rename from frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.html rename to frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.html diff --git a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.scss b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.scss similarity index 99% rename from frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.scss rename to frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.scss index cd48f42fa..919fdec4a 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.scss +++ b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.scss @@ -4,7 +4,6 @@ width: 120px; margin-left: 4em; margin-right: 1.5em; - padding-bottom: 63px; .column { width: 100%; diff --git a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts similarity index 93% rename from frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.ts rename to frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts index ebfa019a1..d85d2ee46 100644 --- a/frontend/src/app/components/accelerate-preview/accelerate-fee-graph.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-fee-graph.component.ts @@ -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, diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html deleted file mode 100644 index 4c92269d8..000000000 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.html +++ /dev/null @@ -1,261 +0,0 @@ - -
-
-
- Transaction has now been submitted to mining pools for acceleration. -
-
-
- - -
-
- -
-
- -
- - - - - -
- -
-
You are currently on the waitlist
-
- -
Your transaction
-
-
- - Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s) - - - - - - - - - - - - - - - - - - -
Virtual size
- Size in vbytes of this transaction (including unconfirmed ancestors) -
In-band fees - {{ estimate.txSummary.effectiveFee | number : '1.0-0' }} sats -
- Fees already paid by this transaction (including unconfirmed ancestors) -
-
-
-
-
How much faster?
-
-
- Your transaction will be prioritized by up to {{ hashratePercentage | number : '1.1-1' }}% of miners. - This will reduce your expected waiting time until the first confirmation to -
-
- -
-
-
-
How much more are you willing to pay?
-
-
- Choose the maximum extra transaction fee you're willing to pay. -
-
-
- - - -
-
-
-
-
- -
Acceleration summary
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Next block market rate - {{ estimate.targetFeeRate | number : '1.0-0' }} - sat/vB
- Estimated extra fee required - - {{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }} - - sats - -
Mempool Accelerator™ fees
- Accelerator Service Fee - - +{{ estimate.mempoolBaseFee | number }} - - sats - -
- Transaction Size Surcharge - - +{{ estimate.vsizeFee | number }} - - sats - -
- Estimated acceleration cost - - - {{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }} - - - sats - -
- -
- Maximum acceleration cost - - - {{ maxCost | number }} - - - sats - - - -
- -
Available balance - {{ estimate.userBalance | number }} - - sats - - - -
- Sign In -
- Accelerate on mempool.space -
-
-
- -
-
-
- -
-
-
- -
-
-
- - -
-
-
- -If your tx is accelerated to ~{{ i | number : '1.0-0' }} sat/vB \ No newline at end of file diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.scss b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.scss deleted file mode 100644 index 1191d882e..000000000 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.scss +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts b/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts deleted file mode 100644 index 6d4c88a00..000000000 --- a/frontend/src/app/components/accelerate-preview/accelerate-preview.component.ts +++ /dev/null @@ -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; - } - } -} diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts index 237b14317..f6224c17d 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts @@ -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 = 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(); } } \ No newline at end of file diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html index 711269a47..927fe1792 100644 --- a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html @@ -4,8 +4,11 @@ - - + + - diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.scss b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.scss index 12849dc65..b01a902a4 100644 --- a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.scss +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.scss @@ -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; + } } \ No newline at end of file diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts index 2d94cad50..90df9987c 100644 --- a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts @@ -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)), + `${pool.name} (${poolShare}%)`, + 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), } ] diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 8fc09345f..08eb841ee 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -26,11 +26,13 @@ - - + @if (!address.electrum) { + + + } @if (network === 'liquid' || network === 'liquidtestnet') { - } @else { + } @else if (!address.electrum) { } @@ -46,17 +48,21 @@ + @if (!address.electrum) { + } @if (network === 'liquid' || network === 'liquidtestnet') { - } @else { + } @else if (!address.electrum) { - } + } @else { + + } @@ -232,6 +238,11 @@ + + + + + diff --git a/frontend/src/app/components/amount/amount.component.html b/frontend/src/app/components/amount/amount.component.html index b38cf4c41..b513c89d2 100644 --- a/frontend/src/app/components/amount/amount.component.html +++ b/frontend/src/app/components/amount/amount.component.html @@ -1,4 +1,4 @@ - + {{ addPlus && satoshis >= 0 ? '+' : '' }}{{ ( @@ -21,7 +21,7 @@ Confidential - @if ((viewAmountMode$ | async) === 'btc' || (viewAmountMode$ | async) === 'fiat') { + @if ((viewAmountMode$ | async) === 'btc' || (viewAmountMode$ | async) === 'fiat' || ignoreViewMode === true) { ‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }} BTC diff --git a/frontend/src/app/components/amount/amount.component.ts b/frontend/src/app/components/amount/amount.component.ts index 60cbe3117..93715f3c0 100644 --- a/frontend/src/app/components/amount/amount.component.ts +++ b/frontend/src/app/components/amount/amount.component.ts @@ -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( diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html new file mode 100644 index 000000000..5fd4f6701 --- /dev/null +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.html @@ -0,0 +1,99 @@ +
+ + @if (!minimal) { + + Payment successful. You can close this page. + + + + A transaction has been detected in the mempool fully paying for this invoice. Waiting for on-chain confirmation. + + } + +
+ +
+ +
+
+ + + +
+
+ + + + + +
+ + + +
+ +
+ +
+ +
+
+ @if (!minimal) { +

{{ loadedInvoice.btcDue | number: '1.0-8' }} BTC

+ } + +
+ + + +
+ + + +
+ +
+ +
+ +
+
+ + @if (!minimal) { +

{{ loadedInvoice.btcDue * 100_000_000 | number: '1.0-0' }} sats

+ } + +
+ + + +
+ + + +
+
+
+ +
+ +
+
+ @if (!minimal) { +

{{ loadedInvoice.btcDue | number: '1.0-8' }} BTC

+ } + +
+ + @if (!minimal) { +

Waiting for transaction...

+
+ } +
+
\ No newline at end of file diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss new file mode 100644 index 000000000..b88a2ef74 --- /dev/null +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.scss @@ -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; +} diff --git a/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts new file mode 100644 index 000000000..cb7e78ebd --- /dev/null +++ b/frontend/src/app/components/bitcoin-invoice/bitcoin-invoice.component.ts @@ -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); + } +} diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.ts b/frontend/src/app/components/blocks-list/blocks-list.component.ts index 29b23e608..9bf4e9814 100644 --- a/frontend/src/app/components/blocks-list/blocks-list.component.ts +++ b/frontend/src/app/components/blocks-list/blocks-list.component.ts @@ -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 { diff --git a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts index b818dff78..31a52fd9d 100644 --- a/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts +++ b/frontend/src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.ts @@ -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]); } } diff --git a/frontend/src/app/components/pool/pool.component.html b/frontend/src/app/components/pool/pool.component.html index a446b552b..bd4f3bbee 100644 --- a/frontend/src/app/components/pool/pool.component.html +++ b/frontend/src/app/components/pool/pool.component.html @@ -97,7 +97,7 @@
- + - +
Accelerated to + Accelerated to + +
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) { @@ -14,7 +17,7 @@ }
+
Confirmed balance
{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}
@@ -219,10 +219,10 @@ - + - + {{ block.tx_count | number }} diff --git a/frontend/src/app/components/qrcode/qrcode.component.scss b/frontend/src/app/components/qrcode/qrcode.component.scss index d4de43026..9c2aafe23 100644 --- a/frontend/src/app/components/qrcode/qrcode.component.scss +++ b/frontend/src/app/components/qrcode/qrcode.component.scss @@ -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 { diff --git a/frontend/src/app/components/qrcode/qrcode.component.ts b/frontend/src/app/components/qrcode/qrcode.component.ts index dad7522c6..f377895c0 100644 --- a/frontend/src/app/components/qrcode/qrcode.component.ts +++ b/frontend/src/app/components/qrcode/qrcode.component.ts @@ -37,7 +37,7 @@ export class QrcodeComponent implements AfterViewInit { return; } const opts: QRCode.QRCodeRenderersOptions = { - errorCorrectionLevel: 'L', + errorCorrectionLevel: 'M', margin: 0, color: { dark: '#000', diff --git a/frontend/src/app/components/rbf-list/rbf-list.component.html b/frontend/src/app/components/rbf-list/rbf-list.component.html index 133cdfe33..a3baa8537 100644 --- a/frontend/src/app/components/rbf-list/rbf-list.component.html +++ b/frontend/src/app/components/rbf-list/rbf-list.component.html @@ -15,7 +15,7 @@
-
+

diff --git a/frontend/src/app/components/rbf-list/rbf-list.component.ts b/frontend/src/app/components/rbf-list/rbf-list.component.ts index ff30dd1c9..25f7dea2e 100644 --- a/frontend/src/app/components/rbf-list/rbf-list.component.ts +++ b/frontend/src/app/components/rbf-list/rbf-list.component.ts @@ -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( diff --git a/frontend/src/app/components/time/time.component.ts b/frontend/src/app/components/time/time.component.ts index 376786d00..979940e8d 100644 --- a/frontend/src/app/components/time/time.component.ts +++ b/frontend/src/app/components/time/time.component.ts @@ -78,6 +78,10 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { } calculate() { + if (!this.time) { + return; + } + let seconds: number; switch (this.kind) { case 'since': diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index 571c02f96..a0f242d46 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -75,9 +75,9 @@ } @else { } - @if (!showAccelerationSummary && isMobile && paymentType === 'cashapp' && accelerationEligible && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) { + @@ -115,8 +115,15 @@

- @if (showAccelerationSummary && !accelerationFlowCompleted) { - + @if (isLoading) { +
+
+
+   + } @else if (showAccelerationSummary) { + + + } @else { @if (tx?.acceleration && !tx.status?.confirmed) {
diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index 62ecc9bf0..349e8d43b 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -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; 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 = ''; diff --git a/frontend/src/app/components/tracker/tracker.module.ts b/frontend/src/app/components/tracker/tracker.module.ts new file mode 100644 index 000000000..799b8cd65 --- /dev/null +++ b/frontend/src/app/components/tracker/tracker.module.ts @@ -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 { } + + + + + + diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 9aa63d7f1..da8763fa6 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -80,11 +80,27 @@

Accelerate

+ + + +
-
- -
+ + + @@ -535,21 +551,23 @@ @if (eta.blocks >= 7) { - + In several hours (or more) - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') { + @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) { Accelerate } } @else if (network === 'liquid' || network === 'liquidtestnet') { } @else { - + - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button') { - Accelerate + @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) { + Accelerate } + + } @@ -648,7 +666,7 @@ - + diff --git a/frontend/src/app/components/transaction/transaction.component.scss b/frontend/src/app/components/transaction/transaction.component.scss index 80caa6003..b43c63c2c 100644 --- a/frontend/src/app/components/transaction/transaction.component.scss +++ b/frontend/src/app/components/transaction/transaction.component.scss @@ -300,7 +300,6 @@ .accelerateDeepMempool { align-self: auto; - margin-top: 3px; margin-left: auto; background-color: var(--tertiary); @media (max-width: 995px) { diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 37c83f008..3bc40ea93 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -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(); diff --git a/frontend/src/app/components/transaction/transaction.module.ts b/frontend/src/app/components/transaction/transaction.module.ts index eb663c9ac..b98c33e2a 100644 --- a/frontend/src/app/components/transaction/transaction.module.ts +++ b/frontend/src/app/components/transaction/transaction.module.ts @@ -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, ] }) diff --git a/frontend/src/app/docs/api-docs/api-docs.component.ts b/frontend/src/app/docs/api-docs/api-docs.component.ts index 76d9de8d0..b655b3969 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.ts +++ b/frontend/src/app/docs/api-docs/api-docs.component.ts @@ -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]; diff --git a/frontend/src/app/docs/code-template/code-template.component.ts b/frontend/src/app/docs/code-template/code-template.component.ts index bd03f5b16..b31def01c 100644 --- a/frontend/src/app/docs/code-template/code-template.component.ts +++ b/frontend/src/app/docs/code-template/code-template.component.ts @@ -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}"`; diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts index 64ccb299f..dd3535f65 100644 --- a/frontend/src/app/graphs/graphs.routing.module.ts +++ b/frontend/src/app/graphs/graphs.routing.module.ts @@ -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'] }, diff --git a/frontend/src/app/liquid/liquid-master-page.module.ts b/frontend/src/app/liquid/liquid-master-page.module.ts index 5df9a5447..3375a066a 100644 --- a/frontend/src/app/liquid/liquid-master-page.module.ts +++ b/frontend/src/app/liquid/liquid-master-page.module.ts @@ -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'] }, diff --git a/frontend/src/app/master-page.module.ts b/frontend/src/app/master-page.module.ts index 6ea8bfd93..510756cbc 100644 --- a/frontend/src/app/master-page.module.ts +++ b/frontend/src/app/master-page.module.ts @@ -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, diff --git a/frontend/src/app/services/eta.service.ts b/frontend/src/app/services/eta.service.ts index 467f49554..cc1436e4c 100644 --- a/frontend/src/app/services/eta.service.ts +++ b/frontend/src/app/services/eta.service.ts @@ -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), - } + }; } } diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index bdc6d18c2..0dc58b957 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -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 { + const params = { + product: txid, + amount: sats, + }; + return this.httpClient.post(`${SERVICES_API_PREFIX}/payments/bitcoin`, params); + } + + retreiveInvoice$(invoiceId: string): Observable { + return this.httpClient.get(`${SERVICES_API_PREFIX}/payments/bitcoin/invoice?id=${invoiceId}`); + } + + getPaymentStatus$(orderId: string): Observable { + return this.httpClient.get(`${SERVICES_API_PREFIX}/payments/bitcoin/check?order_id=${orderId}`); + } } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 0d502747c..647e5a972 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -150,7 +150,7 @@ export class StateService { utxoSpent$ = new Subject(); difficultyAdjustment$ = new ReplaySubject(1); mempoolTransactions$ = new Subject(); - 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(); multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>(); blockTransactions$ = new Subject(); diff --git a/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts b/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts index e60c7c524..1706be24d 100644 --- a/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts +++ b/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts @@ -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) { diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 2f7bd4dc4..c060bbbd2 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -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, diff --git a/frontend/src/app/shared/transaction.utils.ts b/frontend/src/app/shared/transaction.utils.ts index 7bc986330..9d9cd801b 100644 --- a/frontend/src/app/shared/transaction.utils.ts +++ b/frontend/src/app/shared/transaction.utils.ts @@ -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; diff --git a/frontend/src/resources/bitcoin-logo.png b/frontend/src/resources/bitcoin-logo.png new file mode 100644 index 000000000..5d7962d2a Binary files /dev/null and b/frontend/src/resources/bitcoin-logo.png differ diff --git a/frontend/src/resources/btcpay.svg b/frontend/src/resources/btcpay.svg new file mode 100644 index 000000000..5d8592b71 --- /dev/null +++ b/frontend/src/resources/btcpay.svg @@ -0,0 +1 @@ +btcpay3 \ No newline at end of file diff --git a/frontend/src/resources/cash-app.svg b/frontend/src/resources/cash-app.svg new file mode 100644 index 000000000..4dc645081 --- /dev/null +++ b/frontend/src/resources/cash-app.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/resources/lightning-logo.png b/frontend/src/resources/lightning-logo.png new file mode 100644 index 000000000..4507ae9ad Binary files /dev/null and b/frontend/src/resources/lightning-logo.png differ diff --git a/frontend/src/resources/profile/coldcard.png b/frontend/src/resources/profile/coldcard.png new file mode 100644 index 000000000..1f4118747 Binary files /dev/null and b/frontend/src/resources/profile/coldcard.png differ diff --git a/unfurler/package-lock.json b/unfurler/package-lock.json index ded1af081..45c40f097 100644 --- a/unfurler/package-lock.json +++ b/unfurler/package-lock.json @@ -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"