Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions .github/workflows/migration-wallet-setup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: Migration Wallet Setup

on:
workflow_call:
inputs:
e2e_branch:
description: "Branch of synonymdev/bitkit-e2e-tests to use"
required: true
type: string
rn_version:
description: "Legacy RN app version to use for setup (e.g., v1.1.6)"
required: false
type: string
default: "v1.1.6"
setup_type:
description: "Wallet setup type (standard | sweep)"
required: true
type: string
scenario_name:
description: "Migration scenario name for artifact naming"
required: true
type: string

env:
TERM: xterm-256color
FORCE_COLOR: 1

jobs:
prepare-wallets:
runs-on: ubuntu-latest

steps:
- name: Clone E2E tests
uses: actions/checkout@v4
with:
repository: synonymdev/bitkit-e2e-tests
path: bitkit-e2e-tests
ref: ${{ inputs.e2e_branch }}

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Download RN app for migration
run: |
mkdir -p bitkit-e2e-tests/aut
curl -L -o bitkit-e2e-tests/aut/bitkit_rn_regtest.apk \
https://github.com/synonymdev/bitkit-e2e-tests/releases/download/migration-rn-regtest/bitkit_rn_regtest_${{ inputs.rn_version }}.apk
# Symlink to bitkit_e2e.apk so wdio.conf.ts can initialize the Appium session
cd bitkit-e2e-tests/aut && ln -sf bitkit_rn_regtest.apk bitkit_e2e.apk

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Cache npm cache
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Install dependencies
working-directory: bitkit-e2e-tests
run: npm ci

- name: Clear previous migration env files
working-directory: bitkit-e2e-tests
run: |
rm -f artifacts/migration_setup_standard.env
rm -f artifacts/migration_setup_sweep.env

- name: Prepare migration wallet 1
continue-on-error: true
id: prepare1
uses: reactivecircus/android-emulator-runner@v2
with:
profile: pixel_6
api-level: 33
arch: x86_64
avd-name: Pixel_6
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-front none
script: cd bitkit-e2e-tests && ./ci_run_android.sh --mochaOpts.grep "${{ inputs.setup_type == 'sweep' && '@migration_setup_sweep' || '@migration_setup_standard' }}"
env:
BACKEND: regtest

- name: Prepare migration wallet 2
continue-on-error: true
id: prepare2
if: steps.prepare1.outcome != 'success'
uses: reactivecircus/android-emulator-runner@v2
with:
profile: pixel_6
api-level: 33
arch: x86_64
avd-name: Pixel_6
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-front none
script: cd bitkit-e2e-tests && ./ci_run_android.sh --mochaOpts.grep "${{ inputs.setup_type == 'sweep' && '@migration_setup_sweep' || '@migration_setup_standard' }}"
env:
BACKEND: regtest

- name: Prepare migration wallet 3
id: prepare3
if: steps.prepare1.outcome != 'success' && steps.prepare2.outcome != 'success'
uses: reactivecircus/android-emulator-runner@v2
with:
profile: pixel_6
api-level: 33
arch: x86_64
avd-name: Pixel_6
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-front none
script: cd bitkit-e2e-tests && ./ci_run_android.sh --mochaOpts.grep "${{ inputs.setup_type == 'sweep' && '@migration_setup_sweep' || '@migration_setup_standard' }}"
env:
BACKEND: regtest

- name: Verify migration env file
run: |
set -euo pipefail
if [[ "${{ inputs.setup_type }}" == "sweep" ]]; then
test -f bitkit-e2e-tests/artifacts/migration_setup_sweep.env
else
test -f bitkit-e2e-tests/artifacts/migration_setup_standard.env
fi

- name: Upload migration env file
uses: actions/upload-artifact@v4
with:
name: migration-env_${{ inputs.rn_version }}_${{ inputs.scenario_name }}
path: bitkit-e2e-tests/artifacts/migration_setup_${{ inputs.setup_type }}.env
2 changes: 1 addition & 1 deletion test/helpers/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export async function multiTap(testId: string, count: number) {
}
}

async function pasteIOSText(testId: string, text: string) {
export async function pasteIOSText(testId: string, text: string) {
if (!driver.isIOS) {
throw new Error('pasteIOSText can only be used on iOS devices');
}
Expand Down
116 changes: 113 additions & 3 deletions test/specs/migration.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from 'node:fs';
import path from 'node:path';
import {
acknowledgeReceivedPayment,
confirmInputOnKeyboard,
Expand All @@ -12,6 +14,7 @@ import {
getReceiveAddress,
getUriFromQRCode,
handleAndroidAlert,
pasteIOSText,
restoreWallet,
sleep,
swipeFullScreen,
Expand Down Expand Up @@ -61,6 +64,10 @@ const TEST_PASSPHRASE = 'supersecret';
// This is needed because RN app on iOS has poor Appium support
const IOS_RN_MNEMONIC = process.env.RN_MNEMONIC;
const IOS_RN_BALANCE = process.env.RN_BALANCE ? parseInt(process.env.RN_BALANCE, 10) : undefined;
const IOS_RN_MNEMONIC_SWEEP = process.env.RN_MNEMONIC_SWEEP;
const IOS_RN_BALANCE_SWEEP = process.env.RN_BALANCE_SWEEP
? parseInt(process.env.RN_BALANCE_SWEEP, 10)
: undefined;

// ============================================================================
// TEST SUITE
Expand All @@ -76,6 +83,46 @@ describe('@migration - Migration from legacy RN app to native app', () => {
await electrumClient?.stop();
});

// --------------------------------------------------------------------------
// Migration Setup: Prepare legacy RN wallets on Android for iOS runs
// --------------------------------------------------------------------------
ciIt('@migration_setup_standard - Prepare legacy RN wallet (Android only)', async () => {
if (driver.isIOS) {
throw new Error('Migration setup should run on Android only.');
}

const { mnemonic, balance } = await setupLegacyWallet({ returnSeed: true });
writeMigrationEnvFile({
fileName: 'migration_setup_standard.env',
mnemonicVar: 'RN_MNEMONIC',
balanceVar: 'RN_BALANCE',
mnemonic,
balance,
});
});

ciIt('@migration_setup_sweep - Prepare legacy sweep wallet (Android only)', async () => {
if (driver.isIOS) {
throw new Error('Migration setup should run on Android only.');
}

const { mnemonic, balance } = await setupWalletWithLegacyFunds({ returnSeed: true });
writeMigrationEnvFile({
fileName: 'migration_setup_sweep.env',
mnemonicVar: 'RN_MNEMONIC_SWEEP',
balanceVar: 'RN_BALANCE_SWEEP',
mnemonic,
balance,
});
});

ciIt('@migration_ios - setupLegacyWallet on iOS', async () => {
// Setup wallet in RN app
const { mnemonic, balance } = await setupLegacyWallet({ returnSeed: true });
console.info(`→ MNEMONIC: ${mnemonic}`);
console.info(`→ BALANCE: ${balance}`);
});

// --------------------------------------------------------------------------
// Migration Scenario 1: Uninstall RN, install Native, restore mnemonic
// --------------------------------------------------------------------------
Expand Down Expand Up @@ -284,6 +331,36 @@ async function setupLegacyWallet(
return { mnemonic, balance };
}

type MigrationEnvFileArgs = {
fileName: string;
mnemonicVar: string;
balanceVar: string;
mnemonic?: string;
balance: number;
};

function writeMigrationEnvFile({
fileName,
mnemonicVar,
balanceVar,
mnemonic,
balance,
}: MigrationEnvFileArgs): void {
if (!mnemonic) {
throw new Error(`Missing mnemonic for ${fileName}`);
}

const filePath = path.join(process.cwd(), 'artifacts', fileName);
fs.mkdirSync(path.dirname(filePath), { recursive: true });

const contents = `${mnemonicVar}="${mnemonic}"\n${balanceVar}="${balance}"\n`;
fs.writeFileSync(filePath, contents, 'utf8');

console.info(`→ Wrote migration env file: ${filePath}`);
console.info(`\nexport ${mnemonicVar}="${mnemonic}"`);
console.info(`export ${balanceVar}="${balance}"\n`);
}

// Amount constants for sweep scenario
const SWEEP_INITIAL_FUND_SATS = 200_000;
const SWEEP_SEND_TO_SELF_SATS = 50_000;
Expand All @@ -300,13 +377,42 @@ const SWEEP_SEND_TO_SELF_SATS = 50_000;
*
* Result: Wallet has funds on legacy address, migration will trigger sweep
*/
async function setupWalletWithLegacyFunds(): Promise<{ balance: number }> {
async function setupWalletWithLegacyFunds(
options: { returnSeed?: boolean } = {}
): Promise<LegacyWalletSetupResult> {
const { returnSeed } = options;
if (driver.isIOS) {
if (!IOS_RN_MNEMONIC_SWEEP || !IOS_RN_BALANCE_SWEEP) {
throw new Error(
'iOS migration sweep tests require RN_MNEMONIC_SWEEP and RN_BALANCE_SWEEP env vars. ' +
'Run Android setup first to prepare the wallet.'
);
}
console.info('=== iOS: Restoring RN sweep wallet from mnemonic (prepared by Android) ===');
console.info(
`→ Mnemonic: ${IOS_RN_MNEMONIC_SWEEP.split(' ').slice(0, 3).join(' ')}...`
);
console.info(`→ Expected balance: ${IOS_RN_BALANCE_SWEEP} sats`);

await installLegacyRnApp();
await restoreRnWallet(IOS_RN_MNEMONIC_SWEEP);

console.info('=== iOS: RN sweep wallet restored ===');
return { mnemonic: IOS_RN_MNEMONIC_SWEEP, balance: IOS_RN_BALANCE_SWEEP };
}

console.info('=== Setting up wallet with legacy funds (sweep scenario) ===');

// Install and create wallet
await installLegacyRnApp();
await createLegacyRnWallet();

let mnemonic: string | undefined;
if (returnSeed) {
mnemonic = await getRnMnemonic();
console.info(`→ Legacy RN sweep wallet mnemonic: ${mnemonic}`);
}

// 1. Fund wallet on native segwit (works with Blocktank)
console.info('→ Step 1: Funding wallet on native segwit...');
await fundRnWallet(SWEEP_INITIAL_FUND_SATS);
Expand All @@ -324,7 +430,7 @@ async function setupWalletWithLegacyFunds(): Promise<{ balance: number }> {

console.info('=== Legacy funds setup complete ===');

return { balance };
return { balance, mnemonic };
}

/**
Expand Down Expand Up @@ -496,7 +602,11 @@ async function restoreRnWallet(
await tap('MultipleDevices-button');

// Enter seed
await typeText('Word-0', mnemonic);
if (driver.isIOS) {
await pasteIOSText('Word-0', mnemonic);
} else {
await typeText('Word-0', mnemonic);
}
await sleep(1500);

// Passphrase if provided
Expand Down