diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index ba9e1eb7b..1447ec4ab 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -105,7 +105,6 @@ jobs: --cache-to "type=local,dest=/tmp/.buildx-cache,mode=max" \ --platform linux/amd64,linux/arm64 \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ - --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ --build-context rustgbt=./rust \ --build-context backend=./backend \ --output "type=registry,push=true" \ diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 73a14ba4e..3cf2923f1 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -406,8 +406,8 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.json(block); - } catch (e) { - handleError(req, res, 500, 'Failed to get block'); + } catch (e: any) { + handleError(req, res, e?.response?.status === 404 ? 404 : 500, 'Failed to get block'); } } diff --git a/backend/src/api/services/wallets.ts b/backend/src/api/services/wallets.ts index dd4d7ebc9..f498a80ad 100644 --- a/backend/src/api/services/wallets.ts +++ b/backend/src/api/services/wallets.ts @@ -30,6 +30,7 @@ const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes class WalletApi { private wallets: Record = {}; private syncing = false; + private lastSync = 0; constructor() { this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => { @@ -47,7 +48,38 @@ class WalletApi { if (!config.WALLETS.ENABLED || this.syncing) { return; } + this.syncing = true; + + if (config.WALLETS.AUTO && (Date.now() - this.lastSync) > POLL_FREQUENCY) { + try { + // update list of active wallets + this.lastSync = Date.now(); + const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets`); + const walletList: string[] = response.data; + if (walletList) { + // create a quick lookup dictionary of active wallets + const newWallets: Record = Object.fromEntries( + walletList.map(wallet => [wallet, true]) + ); + for (const wallet of walletList) { + // don't overwrite existing wallets + if (!this.wallets[wallet]) { + this.wallets[wallet] = { name: wallet, addresses: {}, lastPoll: 0 }; + } + } + // remove wallets that are no longer active + for (const wallet of Object.keys(this.wallets)) { + if (!newWallets[wallet]) { + delete this.wallets[wallet]; + } + } + } + } catch (e) { + logger.err(`Error updating active wallets: ${(e instanceof Error ? e.message : e)}`); + } + } + for (const walletKey of Object.keys(this.wallets)) { const wallet = this.wallets[walletKey]; if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) { @@ -72,6 +104,7 @@ class WalletApi { } } } + this.syncing = false; } diff --git a/backend/src/config.ts b/backend/src/config.ts index a1050a7d5..3fe3db2ee 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -164,6 +164,7 @@ interface IConfig { }, WALLETS: { ENABLED: boolean; + AUTO: boolean; WALLETS: string[]; }, STRATUM: { @@ -334,6 +335,7 @@ const defaults: IConfig = { }, 'WALLETS': { 'ENABLED': false, + 'AUTO': false, 'WALLETS': [], }, 'STRATUM': { diff --git a/frontend/custom-sv-config.json b/frontend/custom-sv-config.json index dee3dab18..9a61704a2 100644 --- a/frontend/custom-sv-config.json +++ b/frontend/custom-sv-config.json @@ -16,10 +16,10 @@ "mobileOrder": 4 }, { - "component": "balance", + "component": "walletBalance", "mobileOrder": 1, "props": { - "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" + "wallet": "ONBTC" } }, { @@ -30,21 +30,22 @@ } }, { - "component": "address", + "component": "wallet", "mobileOrder": 2, "props": { - "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo", - "period": "1m" + "wallet": "ONBTC", + "period": "1m", + "label": "bitcoin.gob.sv" } }, { "component": "blocks" }, { - "component": "addressTransactions", + "component": "walletTransactions", "mobileOrder": 3, "props": { - "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" + "wallet": "ONBTC" } } ] diff --git a/frontend/src/app/shared/script.utils.ts b/frontend/src/app/shared/script.utils.ts index 62a7a5845..8453bdd63 100644 --- a/frontend/src/app/shared/script.utils.ts +++ b/frontend/src/app/shared/script.utils.ts @@ -256,6 +256,11 @@ export function detectScriptTemplate(type: ScriptType, script_asm: string, witne return ScriptTemplates.multisig(tapscriptMultisig.m, tapscriptMultisig.n); } + const tapscriptUnanimousMultisig = parseTapscriptUnanimousMultisig(script_asm); + if (tapscriptUnanimousMultisig) { + return ScriptTemplates.multisig(tapscriptUnanimousMultisig, tapscriptUnanimousMultisig); + } + return; } @@ -310,11 +315,13 @@ export function parseTapscriptMultisig(script: string): undefined | { m: number, } const ops = script.split(' '); - // At minimum, one pubkey group (3 tokens) + m push + final opcode = 5 tokens - if (ops.length < 5) return; + // At minimum, 2 pubkey group (3 tokens) + m push + final opcode = 8 tokens + if (ops.length < 8) { + return; + } const finalOp = ops.pop(); - if (finalOp !== 'OP_NUMEQUAL' && finalOp !== 'OP_GREATERTHANOREQUAL') { + if (!['OP_NUMEQUAL', 'OP_NUMEQUALVERIFY', 'OP_GREATERTHANOREQUAL', 'OP_GREATERTHAN', 'OP_EQUAL', 'OP_EQUALVERIFY'].includes(finalOp)) { return; } @@ -329,6 +336,10 @@ export function parseTapscriptMultisig(script: string): undefined | { m: number, return; } + if (finalOp === 'OP_GREATERTHAN') { + m += 1; + } + if (ops.length % 3 !== 0) { return; } @@ -360,6 +371,53 @@ export function parseTapscriptMultisig(script: string): undefined | { m: number, return { m, n }; } +export function parseTapscriptUnanimousMultisig(script: string): undefined | number { + if (!script) { + return; + } + + const ops = script.split(' '); + // At minimum, 2 pubkey group (3 tokens) = 6 tokens + if (ops.length < 6) { + return; + } + + if (ops.length % 3 !== 0) { + return; + } + + const n = ops.length / 3; + + for (let i = 0; i < n; i++) { + const pushOp = ops.shift(); + const pubkey = ops.shift(); + const sigOp = ops.shift(); + + if (pushOp !== 'OP_PUSHBYTES_32') { + return; + } + if (!/^[0-9a-fA-F]{64}$/.test(pubkey)) { + return; + } + if (i < n - 1) { + if (sigOp !== 'OP_CHECKSIGVERIFY') { + return; + } + } else { + // Last opcode can be either CHECKSIG or CHECKSIGVERIFY + if (!(sigOp === 'OP_CHECKSIGVERIFY' || sigOp === 'OP_CHECKSIG')) { + return; + } + } + } + + if (ops.length) { + return; + } + + return n; +} + export function getVarIntLength(n: number): number { if (n < 0xfd) { return 1; diff --git a/production/bitcoin.crontab b/production/bitcoin.crontab index a5bc64241..63df3c52a 100644 --- a/production/bitcoin.crontab +++ b/production/bitcoin.crontab @@ -1,7 +1,16 @@ -@reboot sleep 5 ; /usr/local/bin/bitcoind -testnet >/dev/null 2>&1 -@reboot sleep 5 ; /usr/local/bin/bitcoind -testnet4 >/dev/null 2>&1 -@reboot sleep 5 ; /usr/local/bin/bitcoind -signet >/dev/null 2>&1 -@reboot sleep 10 ; screen -dmS mainnet /bitcoin/electrs/start mainnet -@reboot sleep 10 ; screen -dmS testnet /bitcoin/electrs/start testnet -@reboot sleep 10 ; screen -dmS testnet4 /bitcoin/electrs/start testnet4 -@reboot sleep 10 ; screen -dmS signet /bitcoin/electrs/start signet +# start test network daemons on boot +@reboot sleep 10 ; /usr/local/bin/bitcoind -testnet >/dev/null 2>&1 +@reboot sleep 20 ; /usr/local/bin/bitcoind -testnet4 >/dev/null 2>&1 +@reboot sleep 30 ; /usr/local/bin/bitcoind -signet >/dev/null 2>&1 + +# start electrs on boot +@reboot sleep 40 ; screen -dmS mainnet /bitcoin/electrs/start mainnet +@reboot sleep 50 ; screen -dmS testnet /bitcoin/electrs/start testnet +@reboot sleep 60 ; screen -dmS testnet4 /bitcoin/electrs/start testnet4 +@reboot sleep 70 ; screen -dmS signet /bitcoin/electrs/start signet + +# daily update of popular-scripts +30 03 * * * $HOME/electrs/start testnet4 popular-scripts >/dev/null 2>&1 +31 03 * * * $HOME/electrs/start testnet popular-scripts >/dev/null 2>&1 +32 03 * * * $HOME/electrs/start signet popular-scripts >/dev/null 2>&1 +33 03 * * * $HOME/electrs/start mainnet popular-scripts >/dev/null 2>&1 diff --git a/production/elements.crontab b/production/elements.crontab index 4f837706e..6590dfbd7 100644 --- a/production/elements.crontab +++ b/production/elements.crontab @@ -8,3 +8,7 @@ # hourly asset update and electrs restart 6 * * * * cd $HOME/asset_registry_db && git pull --quiet origin master && cd $HOME/asset_registry_testnet_db && git pull --quiet origin master && killall electrs + +# daily update of popular-scripts +32 03 * * * $HOME/electrs/start liquid popular-scripts >/dev/null 2>&1 +33 03 * * * $HOME/electrs/start liquidtestnet popular-scripts >/dev/null 2>&1 diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json index 9505601d2..87f58f916 100644 --- a/production/mempool-config.mainnet.json +++ b/production/mempool-config.mainnet.json @@ -159,6 +159,7 @@ }, "WALLETS": { "ENABLED": true, + "AUTO": true, "WALLETS": ["BITB", "3350"] }, "STRATUM": { diff --git a/production/nginx-cache-heater b/production/nginx-cache-heater index 24ec8a061..e6dea270a 100755 --- a/production/nginx-cache-heater +++ b/production/nginx-cache-heater @@ -4,7 +4,7 @@ hostname=$(hostname) heat() { echo "$1" - curl -i -s "$1" | head -1 + curl -o /dev/null -s "$1" } heatURLs=( diff --git a/production/nginx-cache-warmer b/production/nginx-cache-warmer index f02091747..171f95430 100755 --- a/production/nginx-cache-warmer +++ b/production/nginx-cache-warmer @@ -6,19 +6,19 @@ slugs=(`curl -sSL https://${hostname}/api/v1/mining/pools/3y|jq -r -S '(.pools[] warmSlurp() { echo "$1" - curl -i -s -H 'User-Agent: Googlebot' "$1" | head -1 + curl -o /dev/null -s -H 'User-Agent: Googlebot' "$1" } warmUnfurl() { echo "$1" - curl -i -s -H 'User-Agent: Twitterbot' "$1" | head -1 + curl -o /dev/null -s -H 'User-Agent: Twitterbot' "$1" } warm() { echo "$1" - curl -i -s "$1" | head -1 + curl -o /dev/null -s "$1" } warmSlurpURLs=(