BlueWallet/tests/unit/bip47.test.ts

366 lines
14 KiB
TypeScript

import BIP47Factory from '@spsina/bip47';
import assert from 'assert';
import * as bitcoin from 'bitcoinjs-lib';
import { ECPairFactory } from 'ecpair';
import ecc from '../../blue_modules/noble_ecc';
import { HDSegwitBech32Wallet, WatchOnlyWallet } from '../../class';
import { CreateTransactionUtxo } from '../../class/wallets/types';
const ECPair = ECPairFactory(ecc);
describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
it('should work', async () => {
const bobWallet = new HDSegwitBech32Wallet();
// @see https://gist.github.com/SamouraiDev/6aad669604c5930864bd
bobWallet.setSecret('reward upper indicate eight swift arch injury crystal super wrestle already dentist');
expect(bobWallet.getBIP47PaymentCode()).toEqual(
'PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97',
);
assert.strictEqual(bobWallet.getBIP47NotificationAddress(), '1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV'); // our notif address
assert.ok(!bobWallet.weOwnAddress('1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW')); // alice notif address, we dont own it
});
it('getters, setters, flags work', async () => {
const w = new HDSegwitBech32Wallet();
await w.generate();
expect(w.allowBIP47()).toEqual(true);
expect(w.isBIP47Enabled()).toEqual(false);
w.switchBIP47(true);
expect(w.isBIP47Enabled()).toEqual(true);
w.switchBIP47(false);
expect(w.isBIP47Enabled()).toEqual(false);
// checking that derived watch-only does not support that:
const ww = new WatchOnlyWallet();
ww.setSecret(w.getXpub());
expect(ww.allowBIP47()).toEqual(false);
});
it('should work (samurai)', async () => {
if (!process.env.BIP47_HD_MNEMONIC) {
console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
return;
}
const w = new HDSegwitBech32Wallet();
w.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[0]);
w.setPassphrase('1');
expect(w.getBIP47PaymentCode()).toEqual(
'PM8TJXuZNUtSibuXKFM6bhCxpNaSye6r4px2GXRV5v86uRdH9Raa8ZtXEkG7S4zLREf4ierjMsxLXSFTbRVUnRmvjw9qnc7zZbyXyBstSmjcb7uVcDYF',
);
expect(w._getExternalAddressByIndex(0)).toEqual('bc1q07l355j4yd5kyut36vjxn2u60d3dknnpt39t6y');
const ourNotificationAddress = w.getBIP47NotificationAddress();
const publicBip47 = BIP47Factory(ecc).fromPaymentCode(w.getBIP47PaymentCode());
expect(ourNotificationAddress).toEqual(publicBip47.getNotificationAddress()); // same address we derived internally for ourselves and from public Payment Code
expect(ourNotificationAddress).toEqual('1EiP2kSqxNqRhn8MPMkrtSEqaWiCWLYyTS'); // our notif address
// since we dont do network calls in unit test we cant get counterparties payment codes from our notif address,
// and thus, dont know collaborative addresses with our payers. lets hardcode our counterparty payment code to test
// this functionality
assert.deepStrictEqual(w.getBIP47SenderPaymentCodes(), []);
w._receive_payment_codes = [
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
];
assert.deepStrictEqual(w.getBIP47SenderPaymentCodes(), [
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
]);
assert.ok(w.weOwnAddress('bc1q57nwf9vfq2qsl80q37wq5h0tjytsk95vgjq4fe'));
const pubkey = w._getPubkeyByAddress('bc1q57nwf9vfq2qsl80q37wq5h0tjytsk95vgjq4fe');
const path = w._getDerivationPathByAddress('bc1q57nwf9vfq2qsl80q37wq5h0tjytsk95vgjq4fe');
assert.ok(pubkey);
assert.ok(path);
const keyPair2 = ECPair.fromWIF(w._getWIFbyAddress('bc1q57nwf9vfq2qsl80q37wq5h0tjytsk95vgjq4fe') || '');
const address = bitcoin.payments.p2wpkh({
pubkey: keyPair2.publicKey,
}).address;
assert.strictEqual(address, 'bc1q57nwf9vfq2qsl80q37wq5h0tjytsk95vgjq4fe');
});
it('should work (sparrow)', async () => {
if (!process.env.BIP47_HD_MNEMONIC) {
console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
return;
}
const w = new HDSegwitBech32Wallet();
w.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[1]);
assert.strictEqual(
w.getXpub(),
'zpub6r4KaQRsLuhHSGx8b9wGHh18UnawBs49jtiDzZYh9DSgKGwD72jWR3v54fkyy1UKVxt9HvCkYHmMAUe2YjKefofWzYp9YD62sUp6nNsEDMs',
);
expect(w.getBIP47PaymentCode()).toEqual(
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
);
const ourNotificationAddress = w.getBIP47NotificationAddress();
const publicBip47 = BIP47Factory(ecc).fromPaymentCode(w.getBIP47PaymentCode());
expect(ourNotificationAddress).toEqual(publicBip47.getNotificationAddress()); // same address we derived internally for ourselves and from public Payment Code
expect(ourNotificationAddress).toEqual('16xPugarxLzuNdhDu6XCMJBsMYrTN2fghN'); // our notif address
});
it('should be able to create notification transaction', async () => {
if (!process.env.BIP47_HD_MNEMONIC) {
console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
return;
}
// whom we are going to notify:
const bip47instanceReceiver = BIP47Factory(ecc).fromBip39Seed(process.env.BIP47_HD_MNEMONIC.split(':')[0], undefined, '1');
// notifier:
const walletSender = new HDSegwitBech32Wallet();
walletSender.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[1]);
walletSender.switchBIP47(true);
// lets produce a notification transaction and verify that receiver can actually use it
// since we cant do network calls, we hardcode our senders so later `_getWIFbyAddress`
// could resolve wif for address deposited by him (funds we want to use reside on addresses from BIP47)
walletSender._receive_payment_codes = [
'PM8TJXuZNUtSibuXKFM6bhCxpNaSye6r4px2GXRV5v86uRdH9Raa8ZtXEkG7S4zLREf4ierjMsxLXSFTbRVUnRmvjw9qnc7zZbyXyBstSmjcb7uVcDYF',
];
const utxos: CreateTransactionUtxo[] = [
{
value: 74822,
address: 'bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt',
txid: '73a2ac70858c5b306b101a861d582f40c456a692096a4e4805aa739258c4400d',
vout: 0,
wif: walletSender._getWIFbyAddress('bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt') + '',
},
{
value: 894626,
address: 'bc1qr60ek5gtjs04akcp9f5x25v5gyp2tmspx78jxl',
txid: '64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f',
vout: 0,
wif: walletSender._getWIFbyAddress('bc1qr60ek5gtjs04akcp9f5x25v5gyp2tmspx78jxl') + '',
},
];
const changeAddress = 'bc1q7vraw79vcf7qhnefeaul578h7vjc7tr95ywfuq';
const { tx, fee } = walletSender.createBip47NotificationTransaction(
utxos,
bip47instanceReceiver.getSerializedPaymentCode(),
33,
changeAddress,
);
assert(tx);
const recoveredPaymentCode = bip47instanceReceiver.getPaymentCodeFromRawNotificationTransaction(tx.toHex());
assert.strictEqual(walletSender.getBIP47PaymentCode(), recoveredPaymentCode); // accepted!
assert.strictEqual(
tx.outs[1].script.toString('hex'),
'6a4c500100031c9282bd392ee9700a50d7161c5f76f7b89e7a6fb551bfd5660e79cc7c8d8e7f7676b25ab4db90a96fadfa1254741e09b35e27c7dc1abcd2dc93c4c32732f45400000000000000000000000000',
);
const actualFeerate = fee / tx.virtualSize();
assert.strictEqual(Math.round(actualFeerate), 33);
});
it('should be able to pay to PC', async () => {
if (!process.env.BIP47_HD_MNEMONIC) {
console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
return;
}
// whom we are going to pay:
const bip47instanceReceiver = BIP47Factory(ecc).fromBip39Seed(process.env.BIP47_HD_MNEMONIC.split(':')[0], undefined, '1');
// notifier:
const walletSender = new HDSegwitBech32Wallet();
walletSender.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[1]);
walletSender.switchBIP47(true);
// since we cant do network calls, we hardcode our senders so later `_getWIFbyAddress`
// could resolve wif for address deposited by him (funds we want to use reside on addresses from BIP47)
walletSender._receive_payment_codes = [
'PM8TJXuZNUtSibuXKFM6bhCxpNaSye6r4px2GXRV5v86uRdH9Raa8ZtXEkG7S4zLREf4ierjMsxLXSFTbRVUnRmvjw9qnc7zZbyXyBstSmjcb7uVcDYF',
];
walletSender.addBIP47Receiver(bip47instanceReceiver.getSerializedPaymentCode());
const utxos: CreateTransactionUtxo[] = [
{
value: 74822,
address: 'bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt',
txid: '73a2ac70858c5b306b101a861d582f40c456a692096a4e4805aa739258c4400d',
vout: 0,
wif: walletSender._getWIFbyAddress('bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt') + '',
},
{
value: 894626,
address: 'bc1qr60ek5gtjs04akcp9f5x25v5gyp2tmspx78jxl',
txid: '64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f',
vout: 0,
wif: walletSender._getWIFbyAddress('bc1qr60ek5gtjs04akcp9f5x25v5gyp2tmspx78jxl') + '',
},
];
const changeAddress = 'bc1q7vraw79vcf7qhnefeaul578h7vjc7tr95ywfuq';
const { tx, fee } = walletSender.createTransaction(
utxos,
[
{ address: bip47instanceReceiver.getSerializedPaymentCode(), value: 10234 },
{ address: '13HaCAB4jf7FYSZexJxoczyDDnutzZigjS', value: 22000 },
],
6,
changeAddress,
);
assert(tx);
assert.strictEqual(tx.outs[0].value, 10234);
assert.strictEqual(
bitcoin.address.fromOutputScript(tx.outs[0].script),
walletSender._getBIP47AddressSend(bip47instanceReceiver.getSerializedPaymentCode(), 0),
);
assert.strictEqual(tx.outs[1].value, 22000);
assert.strictEqual(bitcoin.address.fromOutputScript(tx.outs[1].script), '13HaCAB4jf7FYSZexJxoczyDDnutzZigjS');
const actualFeerate = fee / tx.virtualSize();
assert.strictEqual(Math.round(actualFeerate), 6);
// lets retry, but pretend that a few sender's addresses were used:
walletSender._next_free_payment_code_address_index_send[bip47instanceReceiver.getSerializedPaymentCode()] = 6;
const { tx: tx2 } = walletSender.createTransaction(
utxos,
[
{ address: bip47instanceReceiver.getSerializedPaymentCode(), value: 10234 },
{ address: '13HaCAB4jf7FYSZexJxoczyDDnutzZigjS', value: 22000 },
],
6,
changeAddress,
);
assert(tx2);
assert.strictEqual(
bitcoin.address.fromOutputScript(tx2.outs[0].script),
walletSender._getBIP47AddressSend(bip47instanceReceiver.getSerializedPaymentCode(), 6),
);
});
it('should be able to pay to PC (BIP-352 SilentPayments)', async () => {
if (!process.env.BIP47_HD_MNEMONIC) {
console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
return;
}
const walletSender = new HDSegwitBech32Wallet();
walletSender.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[1]);
walletSender.switchBIP47(true);
const utxos: CreateTransactionUtxo[] = [
{
txid: 'ff2b3dc0f16ad96e48f59232421113330781a88ca9b4518846ad9a626260abd3',
vout: 1,
address: 'bc1qr7trw22djl93c2vz43ftlmaexhvph8w0v4f6ap',
value: 195928,
wif: walletSender._getWIFbyAddress('bc1qr7trw22djl93c2vz43ftlmaexhvph8w0v4f6ap') as string,
},
];
const changeAddress = 'bc1q7vraw79vcf7qhnefeaul578h7vjc7tr95ywfuq';
const { tx, fee } = walletSender.createTransaction(
utxos,
[
{ address: '13HaCAB4jf7FYSZexJxoczyDDnutzZigjS', value: 22000 },
{
address: 'sp1qqvvnsd3xnjpmx8hnn2ua0e9sllm34t9jydf8qfesgc7nhdxgzksjwqlrxx37nfzsg6rure5vwa92fksd6f5a6rk05kr07twhd55u3ahquy2v7t6s',
value: 10234,
},
],
6,
changeAddress,
);
assert(tx);
const legacyAddressDestination = tx.outs.find(o => bitcoin.address.fromOutputScript(o.script) === '13HaCAB4jf7FYSZexJxoczyDDnutzZigjS');
assert.strictEqual(legacyAddressDestination?.value, 22000);
const spDestinatiob = tx.outs.find(o => o.value === 10234);
assert.strictEqual(
bitcoin.address.fromOutputScript(spDestinatiob!.script!),
'bc1pu7dwaehvur4lpc7cqmynnjgx5ngthk574p05mgwxf9lecv4r6j5s02nhxq',
);
const changeDestination = tx.outs.find(
o => bitcoin.address.fromOutputScript(o.script) === 'bc1q7vraw79vcf7qhnefeaul578h7vjc7tr95ywfuq',
);
const calculatedFee = 195928 - changeDestination!.value - spDestinatiob!.value - legacyAddressDestination!.value;
assert.strictEqual(fee, calculatedFee);
const actualFeerate = fee / tx.virtualSize();
assert.strictEqual(Math.round(actualFeerate), 6);
});
it('can unwrap addresses to send & receive', () => {
if (!process.env.BIP47_HD_MNEMONIC) {
console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
return;
}
const w = new HDSegwitBech32Wallet();
w.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[0]);
w.setPassphrase('1');
const addr = w._getBIP47AddressReceive(
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
0,
);
assert.strictEqual(addr, 'bc1q57nwf9vfq2qsl80q37wq5h0tjytsk95vgjq4fe');
const addr2 = w._getBIP47AddressSend(
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
0,
);
assert.strictEqual(addr2, 'bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt');
assert.strictEqual(w.getAllExternalAddresses().length, 20); // exactly gap limit for external addresses
assert.ok(!w.getAllExternalAddresses().includes(addr)); // joint address to _receive_ is not included
// since we dont do network calls in unit test we cant get counterparties payment codes from our notif address,
// and thus, dont know collaborative addresses with our payers. lets hardcode our counterparty payment code to test
// this functionality
assert.deepStrictEqual(w.getBIP47SenderPaymentCodes(), []);
w.switchBIP47(true);
w._receive_payment_codes = [
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
];
assert.deepStrictEqual(w.getBIP47SenderPaymentCodes(), [
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
]);
assert.ok(w.getAllExternalAddresses().includes(addr)); // joint address to _receive_ is included
assert.ok(w.getAllExternalAddresses().length > 20);
});
});