diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d3d9abd..4df8ccf 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -29,9 +29,16 @@ jobs: - uses: actions/setup-go@v6 with: go-version: '1.26.3' + - uses: actions/setup-node@v4 + with: + node-version: '20' - - name: Run Nigiri - uses: vulpemventures/nigiri-github-action@v1 + - name: Start regtest base stack (Bitcoin Core + Fulcrum + mempool) + run: | + # Temporary: track the unmerged #27 branch so regtest.mjs exists. + # Switch back to master once ArkLabsHQ/arkade-regtest#27 merges. + git clone --branch denigiri-regtest https://github.com/ArkLabsHQ/arkade-regtest.git regtest + REGTEST_PROFILES=base node regtest/regtest.mjs start - name: Run regtest environment run: make docker-run @@ -50,3 +57,7 @@ jobs: - name: Tear down if: always() run: make docker-stop + + - name: Tear down regtest stack + if: always() + run: node regtest/regtest.mjs clean || true diff --git a/README.md b/README.md index 8aa86a5..7859529 100644 --- a/README.md +++ b/README.md @@ -197,8 +197,8 @@ The service can be configured using environment variables: - Go 1.26+ - Docker and Docker Compose +- Node.js 18+ (to drive the [arkade-regtest](https://github.com/ArkLabsHQ/arkade-regtest) stack for integration testing) - Buf CLI (for protocol buffer generation) -- [Nigiri](https://nigiri.vulpem.com) (for integration testing) ### Building @@ -223,12 +223,22 @@ make run # Run unit tests make test -# Run docker regtest environment -nigiri start +# Bring up the regtest base stack (Bitcoin Core + Fulcrum + mempool) via the +# arkade-regtest Node CLI (replaces `nigiri start`). Esplora's REST API is served +# by mempool under /api (http://localhost:3000/api on the host, +# http://mempool_web/api in-network). +git clone https://github.com/ArkLabsHQ/arkade-regtest.git regtest +REGTEST_PROFILES=base node regtest/regtest.mjs start + +# Run the emulator + arkd docker environment make docker-run # Run integration tests make integrationtest + +# Tear down +make docker-stop +node regtest/regtest.mjs clean ``` ## Supported Opcodes diff --git a/docker-compose.regtest.yml b/docker-compose.regtest.yml index 81b43db..ba710b1 100644 --- a/docker-compose.regtest.yml +++ b/docker-compose.regtest.yml @@ -34,12 +34,13 @@ services: interval: 2s timeout: 2s retries: 30 - nbxplorer: + # Renamed from `nbxplorer` (+ host port dropped) to avoid colliding with the + # arkade-regtest base stack's own `nbxplorer` container/port on the shared + # network. Only arkd-wallet talks to it, in-network. + emulator-nbxplorer: restart: unless-stopped - container_name: nbxplorer - ports: - - 32838:32838 - image: nicolasdorier/nbxplorer:2.5.30 + container_name: emulator-nbxplorer + image: nicolasdorier/nbxplorer:2.6.7 environment: - NBXPLORER_NETWORK=regtest - NBXPLORER_CHAINS=btc @@ -63,15 +64,15 @@ services: arkd-wallet: restart: unless-stopped - image: ghcr.io/arkade-os/arkd-wallet:v0.9.3 + image: ghcr.io/arkade-os/arkd-wallet:v0.9.6 container_name: arkd-wallet depends_on: - - nbxplorer + - emulator-nbxplorer ports: - "6060:6060" environment: - ARKD_WALLET_LOG_LEVEL=5 - - ARKD_WALLET_NBXPLORER_URL=http://nbxplorer:32838 + - ARKD_WALLET_NBXPLORER_URL=http://emulator-nbxplorer:32838 - ARKD_WALLET_DATADIR=./data/regtest - ARKD_WALLET_NETWORK=regtest - ARKD_WALLET_SIGNER_KEY=afcd3fa10f82a05fddc9574fdb13b3991b568e89cc39a72ba4401df8abef35f0 @@ -80,7 +81,7 @@ services: target: /app/data arkd: build: - context: https://github.com/arkade-os/arkd.git#v0.9.3 + context: https://github.com/arkade-os/arkd.git#v0.9.6 dockerfile: Dockerfile container_name: arkd restart: unless-stopped @@ -99,7 +100,7 @@ services: - ARKD_CHECKPOINT_EXIT_DELAY=512 - ARKD_DATADIR=./data/regtest - ARKD_WALLET_ADDR=arkd-wallet:6060 - - ARKD_ESPLORA_URL=http://chopsticks:3000 + - ARKD_ESPLORA_URL=http://mempool_web/api - ARKD_ROUND_MIN_PARTICIPANTS_COUNT=${ARKD_ROUND_MIN_PARTICIPANTS_COUNT:-1} - ARKD_ROUND_MAX_PARTICIPANTS_COUNT=${ARKD_ROUND_MAX_PARTICIPANTS_COUNT:-128} - ARKD_VTXO_MIN_AMOUNT=1 @@ -113,7 +114,11 @@ services: - type: tmpfs target: /app/data +# Join the network created by the arkade-regtest stack so this compose's arkd / +# arkd-wallet can reach the shared bitcoin, nbxplorer and mempool_web containers. +# Docker Compose names the default network "_default"; arkade-regtest's +# compose project is "arkade-regtest", hence "arkade-regtest_default". networks: default: - name: nigiri + name: arkade-regtest_default external: true diff --git a/test/asset_account_covenant_test.go b/test/asset_account_covenant_test.go index d1c4ba2..d8551e9 100644 --- a/test/asset_account_covenant_test.go +++ b/test/asset_account_covenant_test.go @@ -90,7 +90,7 @@ func TestAssetAccountCovenant(t *testing.T) { }) indexerSvc := setupIndexer(t) - explorerSvc, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorerSvc, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) infos, err := grpcAlice.GetInfo(ctx) diff --git a/test/asset_test.go b/test/asset_test.go index 0d45d63..422e4c3 100644 --- a/test/asset_test.go +++ b/test/asset_test.go @@ -157,7 +157,7 @@ func TestOffchainTxWithAsset(t *testing.T) { encodedValidTx, err := validTx.B64Encode() require.NoError(t, err) - explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) signedTx, err := bobWallet.SignTransaction( @@ -216,7 +216,7 @@ func TestSettlementWithAsset(t *testing.T) { conn.Close() }) - explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) // ========================================================================= diff --git a/test/batch_continuation_test.go b/test/batch_continuation_test.go index c3c7502..753b9b2 100644 --- a/test/batch_continuation_test.go +++ b/test/batch_continuation_test.go @@ -65,7 +65,7 @@ func TestCounterContractBatchContinuation(t *testing.T) { indexerSvc := setupIndexer(t) - explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) // ========================================================================= diff --git a/test/contract_id_test.go b/test/contract_id_test.go index 86f5111..da2c1e1 100644 --- a/test/contract_id_test.go +++ b/test/contract_id_test.go @@ -56,7 +56,7 @@ func TestContractIdWithAssetIdentity(t *testing.T) { require.NoError(t, err) indexerSvc := setupIndexer(t) - explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) // Recipient for the reader's value after the co-spend. diff --git a/test/cross_input_script_validation_test.go b/test/cross_input_script_validation_test.go index 68dafa0..aa50f87 100644 --- a/test/cross_input_script_validation_test.go +++ b/test/cross_input_script_validation_test.go @@ -414,7 +414,7 @@ func newCrossInputTestEnv(t *testing.T) *crossInputTestEnv { indexerSvc := setupIndexer(t) - explorerSvc, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorerSvc, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) recipientPrivKey, err := btcec.NewPrivateKey() diff --git a/test/delegate_test.go b/test/delegate_test.go index 3e2827b..cf68372 100644 --- a/test/delegate_test.go +++ b/test/delegate_test.go @@ -85,7 +85,7 @@ func TestCovenantDelegate(t *testing.T) { indexerSvc := setupIndexer(t) explorerSvc, err := mempoolexplorer.NewExplorer( - "http://localhost:3000", arklib.BitcoinRegTest, + "http://localhost:3000/api", arklib.BitcoinRegTest, ) require.NoError(t, err) diff --git a/test/deprecated_key_signing_test.go b/test/deprecated_key_signing_test.go index f4aee75..96f5d44 100644 --- a/test/deprecated_key_signing_test.go +++ b/test/deprecated_key_signing_test.go @@ -73,7 +73,7 @@ func runSubmitTxWithDeprecatedKey( bobWallet, _, bobPubKey := setupWallet(t, ctx) aliceAddr := fundAndSettleAlice(t, ctx, alice, 10_000) indexerSvc := setupIndexer(t) - explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) alicePkScript, err := script.P2TRScript(aliceAddr.VtxoTapKey) @@ -164,7 +164,7 @@ func runSubmitIntentFinalizationWithDeprecatedKey( t.Cleanup(func() { grpcClient.Close() }) aliceAddr := fundAndSettleAlice(t, ctx, alice, 100_000) indexerSvc := setupIndexer(t) - explorerSvc, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorerSvc, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) delegateArkadeScript := enforceSelfSend(t) @@ -325,13 +325,12 @@ func runSubmitOnchainWithDeprecatedKey( tapAddr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(vtxoTapKey), getRegtestParams(t)) require.NoError(t, err) - _, err = runCommand("nigiri", "faucet", tapAddr.EncodeAddress(), "0.01") + _, err = onchainFaucet(tapAddr.EncodeAddress(), "0.01") require.NoError(t, err) - explorerSvc, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorerSvc, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) fundingUtxo := waitForUtxo(t, explorerSvc, tapAddr.EncodeAddress(), 60*time.Second) - _, err = runCommand("nigiri", "rpc", "-generate", "1") - require.NoError(t, err) + require.NoError(t, mineBlocks(1)) rawFundingHex, err := explorerSvc.GetTxHex(fundingUtxo.Txid) require.NoError(t, err) rawFundingBytes, err := hex.DecodeString(rawFundingHex) diff --git a/test/onchain_test.go b/test/onchain_test.go index 55bd8a3..beae40d 100644 --- a/test/onchain_test.go +++ b/test/onchain_test.go @@ -25,9 +25,9 @@ import ( "github.com/stretchr/testify/require" ) -// TestSubmitOnchainTx funds an arkade-tweaked P2TR address directly via -// `nigiri faucet`, then spends it via a plain Bitcoin transaction where the -// emulator co-signs after running the embedded arkade script. +// TestSubmitOnchainTx funds an arkade-tweaked P2TR address directly via the +// regtest Bitcoin Core node, then spends it via a plain Bitcoin transaction +// where the emulator co-signs after running the embedded arkade script. func TestSubmitOnchainTx(t *testing.T) { ctx := context.Background() @@ -93,12 +93,12 @@ func TestSubmitOnchainTx(t *testing.T) { require.NoError(t, err) tapAddrStr := tapAddr.EncodeAddress() - // --- Fund the address via nigiri --- - _, err = runCommand("nigiri", "faucet", tapAddrStr, "0.01") + // --- Fund the address via the regtest Bitcoin Core node --- + _, err = onchainFaucet(tapAddrStr, "0.01") require.NoError(t, err) explorerSvc, err := mempoolexplorer.NewExplorer( - "http://localhost:3000", arklib.BitcoinRegTest, + "http://localhost:3000/api", arklib.BitcoinRegTest, ) require.NoError(t, err) @@ -106,8 +106,7 @@ func TestSubmitOnchainTx(t *testing.T) { fundingUtxo := waitForUtxo(t, explorerSvc, tapAddrStr, 60*time.Second) // Mine a block so the funding tx is confirmed. - _, err = runCommand("nigiri", "rpc", "-generate", "1") - require.NoError(t, err) + require.NoError(t, mineBlocks(1)) rawFundingHex, err := explorerSvc.GetTxHex(fundingUtxo.Txid) require.NoError(t, err) @@ -337,16 +336,13 @@ func TestSubmitOnchainTx(t *testing.T) { require.NoError(t, err) exitAddrStr := exitAddr.EncodeAddress() - _, err = runCommand("nigiri", "faucet", exitAddrStr, "0.01") + _, err = onchainFaucet(exitAddrStr, "0.01") require.NoError(t, err) exitUtxo := waitForUtxo(t, explorerSvc, exitAddrStr, 60*time.Second) // Mine CSV + 1 blocks so the relative locktime is satisfied. - for i := uint32(0); i < csvBlocks+1; i++ { - _, err = runCommand("nigiri", "rpc", "-generate", "1") - require.NoError(t, err) - } + require.NoError(t, mineBlocks(int(csvBlocks+1))) exitRawHex, err := explorerSvc.GetTxHex(exitUtxo.Txid) require.NoError(t, err) diff --git a/test/pay_2_out_test.go b/test/pay_2_out_test.go index 5a9ff13..6c2c36c 100644 --- a/test/pay_2_out_test.go +++ b/test/pay_2_out_test.go @@ -72,7 +72,7 @@ func TestPayToTwoOutputs(t *testing.T) { aliceAddr, err := arklib.DecodeAddressV0(offchainAddr) require.NoError(t, err) - _, err = runCommand("nigiri", "faucet", boardingAddress) + _, err = onchainFaucet(boardingAddress, "") require.NoError(t, err) time.Sleep(5 * time.Second) @@ -229,7 +229,7 @@ func TestPayToTwoOutputs(t *testing.T) { RevealedTapscripts: []string{hex.EncodeToString(arkadeTapscript)}, } - explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) // ======================================== diff --git a/test/recursive_covenant_test.go b/test/recursive_covenant_test.go index 86583c3..54bf022 100644 --- a/test/recursive_covenant_test.go +++ b/test/recursive_covenant_test.go @@ -179,7 +179,7 @@ func TestRecursivePolicy(t *testing.T) { alicePkScript, err := txscript.PayToTaprootScript(alicePubKey) require.NoError(t, err) - explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) submitAndFinalize := func(candidateTx *psbt.Packet, checkpoints []*psbt.Packet) { diff --git a/test/signed_pay_to_output_test.go b/test/signed_pay_to_output_test.go index aaaeaa5..61a68e9 100644 --- a/test/signed_pay_to_output_test.go +++ b/test/signed_pay_to_output_test.go @@ -44,7 +44,7 @@ func TestSignedPayToOutput(t *testing.T) { indexerSvc := setupIndexer(t) explorerSvc, err := mempoolexplorer.NewExplorer( - "http://localhost:3000", arklib.BitcoinRegTest, + "http://localhost:3000/api", arklib.BitcoinRegTest, ) require.NoError(t, err) diff --git a/test/tx_test.go b/test/tx_test.go index 850078b..8414f13 100644 --- a/test/tx_test.go +++ b/test/tx_test.go @@ -12,6 +12,7 @@ import ( "net/http" "os" "os/exec" + "strconv" "strings" "sync" "testing" @@ -90,7 +91,7 @@ func TestSubmitOffchain(t *testing.T) { altIntroWallet, _, altIntroPubKey := setupWallet(t, ctx) emulatorClient, emulatorPublicKey, _ := setupEmulatorClient(t, ctx) indexerSvc := setupIndexer(t) - explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) bobPaysAliceContract := createVtxoScriptWithArkadeScript(bobPubKey, aliceAddr.Signer, emulatorPublicKey, arkade.ArkadeScriptHash(mustPayAliceScript)) @@ -724,7 +725,7 @@ func TestSettlement(t *testing.T) { encodedIntentProof, err := intentPtx.B64Encode() require.NoError(t, err) - explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) signedIntentProof, err := bobWallet.SignTransaction(ctx, explorer, encodedIntentProof) @@ -892,15 +893,13 @@ func TestBoarding(t *testing.T) { require.NoError(t, err) // faucet the contract address with onchain funds - faucetOutput, err := runCommand("nigiri", "faucet", contractBtcAddr.EncodeAddress()) + faucetTxid, err := onchainFaucet(contractBtcAddr.EncodeAddress(), "") require.NoError(t, err) - faucetTxid := strings.TrimSpace(strings.TrimPrefix(faucetOutput, "txId:")) - time.Sleep(5 * time.Second) // get the raw transaction to find the contract output - rawTxHex, err := runCommand("nigiri", "rpc", "getrawtransaction", faucetTxid) + rawTxHex, err := bitcoinCli("getrawtransaction", faucetTxid) require.NoError(t, err) rawTxBytes, err := hex.DecodeString(strings.TrimSpace(rawTxHex)) @@ -991,7 +990,7 @@ func TestBoarding(t *testing.T) { encodedIntentProof, err := intentPtx.B64Encode() require.NoError(t, err) - explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) signedIntentProof, err := bobWallet.SignTransaction(ctx, explorer, encodedIntentProof) @@ -1148,7 +1147,7 @@ func TestEmulatorRejectsInvalidArkadeScript(t *testing.T) { checkpointScriptBytes, err := hex.DecodeString(infos.CheckpointTapscript) require.NoError(t, err) - explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorer, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) invalidArkadeScript, err := txscript.NewScriptBuilder(). @@ -1407,8 +1406,7 @@ func runCommand(name string, arg ...string) (string, error) { } func generateBlock() error { - _, err := runCommand("nigiri", "rpc", "--generate", "1") - return err + return mineBlocks(1) } var ErrAlreadySetup = errors.New("already setup") @@ -1518,8 +1516,7 @@ func setupServerWalletAndCLI() error { const numberOfFaucet = 15 // must cover the liquidity needed for all tests for i := 0; i < numberOfFaucet; i++ { - _, err = runCommand("nigiri", "faucet", addr.Address) - if err != nil { + if _, err = onchainFaucet(addr.Address, ""); err != nil { return fmt.Errorf("failed to fund wallet: %s", err) } } @@ -1528,7 +1525,7 @@ func setupServerWalletAndCLI() error { if _, err := runArkCommand( "init", "--server-url", "localhost:7070", "--password", password, - "--explorer", "http://chopsticks:3000", + "--explorer", "http://mempool_web/api", ); err != nil { return fmt.Errorf("error initializing ark config: %s", err) } @@ -1558,6 +1555,52 @@ func runArkCommand(arg ...string) (string, error) { return runDockerExec("arkd", args...) } +// bitcoinCliArgs is the bitcoin-cli prefix used to drive the regtest Bitcoin +// Core node inside the `bitcoin` container of the arkade-regtest stack. +var bitcoinCliArgs = []string{ + "exec", "bitcoin", + "bitcoin-cli", "-regtest", "-rpcuser=admin1", "-rpcpassword=123", +} + +// bitcoinCli runs `bitcoin-cli ` inside the `bitcoin` container and +// returns the trimmed stdout. It replaces the old `nigiri rpc ` +// passthrough. +func bitcoinCli(arg ...string) (string, error) { + args := append(append([]string{}, bitcoinCliArgs...), arg...) + out, err := runCommand("docker", args...) + if err != nil { + return "", err + } + return strings.TrimSpace(out), nil +} + +// mineBlocks mines n blocks to the node wallet (bitcoin-cli -generate n), +// replacing the old `nigiri rpc -generate n`. +func mineBlocks(n int) error { + _, err := bitcoinCli("-generate", strconv.Itoa(n)) + return err +} + +// onchainFaucet sends amountBtc to address from the Bitcoin Core node wallet +// and mines one block to confirm it, then returns the funding txid. It replaces +// the old `nigiri faucet
` (nigiri auto-mined; the +// arkade-regtest stack does not, so we mine 1 block here to preserve the +// confirm-on-faucet behavior). Defaults to 1 BTC when amountBtc is empty, +// matching nigiri's default faucet amount. +func onchainFaucet(address, amountBtc string) (string, error) { + if amountBtc == "" { + amountBtc = "1" + } + txid, err := bitcoinCli("sendtoaddress", address, amountBtc) + if err != nil { + return "", err + } + if err := mineBlocks(1); err != nil { + return "", err + } + return strings.TrimSpace(txid), nil +} + func runDockerExec(container string, arg ...string) (string, error) { args := append([]string{"exec", "-t", container}, arg...) out, err := runCommand("docker", args...) diff --git a/test/utils_test.go b/test/utils_test.go index 535945c..7109c41 100644 --- a/test/utils_test.go +++ b/test/utils_test.go @@ -481,7 +481,7 @@ func fundAndSettleAlice(t *testing.T, ctx context.Context, alice arksdk.ArkClien amountBtc := strings.TrimSuffix(btcutil.Amount(amount).Format(btcutil.AmountBTC), " BTC") - _, err = runCommand("nigiri", "faucet", boardingAddress, amountBtc) + _, err = onchainFaucet(boardingAddress, amountBtc) require.NoError(t, err) time.Sleep(5 * time.Second) @@ -603,7 +603,7 @@ func submitWithArkd( ) { t.Helper() - explorerSvc, err := mempoolexplorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + explorerSvc, err := mempoolexplorer.NewExplorer("http://localhost:3000/api", arklib.BitcoinRegTest) require.NoError(t, err) encodedTx, err := candidateTx.B64Encode()