Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Qi Wallet v2 #329

Merged
merged 23 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
96fdb0f
add new types
alejoacosta74 Oct 21, 2024
a4dd595
unify data structures into single map
alejoacosta74 Oct 21, 2024
6a3d082
refactor address getter methods
alejoacosta74 Oct 21, 2024
4874610
update sendTx to use new data structure
alejoacosta74 Oct 21, 2024
910822d
update scanning & syncing logic
alejoacosta74 Oct 21, 2024
2283247
update addresses get methods
alejoacosta74 Oct 21, 2024
d859152
update serialization logic
alejoacosta74 Oct 21, 2024
9b49c33
update paymentcode logic
alejoacosta74 Oct 21, 2024
e9f5413
fix bug in getNext payment code address
alejoacosta74 Oct 21, 2024
e1119fa
remove helper methods
alejoacosta74 Oct 21, 2024
37e3259
Fixes to new qi syncing logic
rileystephens28 Oct 22, 2024
1bc903a
Temporarily add legacy qi wallet for comparison
rileystephens28 Oct 22, 2024
55b75ab
bug fix in _findLastUsedIndex
alejoacosta74 Oct 22, 2024
23fd54c
QiHDWalletLegacy: bug fix on paymentcode addr generation
alejoacosta74 Oct 22, 2024
8d34625
update serialization/deserialization properties
alejoacosta74 Oct 22, 2024
1b0630a
add script to compare new vs legacy QiHDWallet address generation
alejoacosta74 Oct 22, 2024
526ddf7
update QiAddressInfo with derivationPath property
alejoacosta74 Oct 22, 2024
f7ebfe1
remove duplicated updates to address map
alejoacosta74 Oct 22, 2024
9d9d4bd
Fix `getAddressesForZone` method name
rileystephens28 Oct 22, 2024
dbf709d
Support min denomination when selecting for conversion txs
rileystephens28 Oct 22, 2024
ae49b30
fix bug in getChangeAddresses function
alejoacosta74 Oct 23, 2024
361ce46
Fix serializing all derivation paths
rileystephens28 Oct 23, 2024
3e27a77
Remove legacy Qi HD Wallet export
rileystephens28 Oct 23, 2024
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
108 changes: 108 additions & 0 deletions examples/wallets/compare-qi-wallets-addresses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const quais = require('../../lib/commonjs/quais');
require('dotenv').config();

async function compareWallets() {
const mnemonic = quais.Mnemonic.fromPhrase(process.env.MNEMONIC);

const newQiWallet = quais.QiHDWallet.fromMnemonic(mnemonic);
const legacyQiWallet = quais.QiHDWalletLegacy.fromMnemonic(mnemonic);

// Create Bob's wallet
const bobMnemonic = quais.Mnemonic.fromPhrase("innocent perfect bus miss prevent night oval position aspect nut angle usage expose grace juice");
const bobNewQiWallet = quais.QiHDWallet.fromMnemonic(bobMnemonic);
const bobLegacyQiWallet = quais.QiHDWalletLegacy.fromMnemonic(bobMnemonic);

const zones = [quais.Zone.Cyprus1, quais.Zone.Cyprus2];
const addressCount = 5;

for (const zone of zones) {
console.log(`Comparing addresses for zone ${zone}:`);

const newAddresses = [];
const legacyAddresses = [];

for (let i = 0; i < addressCount; i++) {
const newAddressInfo = await newQiWallet.getNextAddress(0, zone);
const legacyAddressInfo = await legacyQiWallet.getNextAddress(0, zone);

newAddresses.push(newAddressInfo);
legacyAddresses.push(legacyAddressInfo);

compareAddressInfo(newAddressInfo, legacyAddressInfo, i);
}

console.log('\nComparing change addresses:');
for (let i = 0; i < addressCount; i++) {
const newChangeAddressInfo = await newQiWallet.getNextChangeAddress(0, zone);
const legacyChangeAddressInfo = await legacyQiWallet.getNextChangeAddress(0, zone);

compareAddressInfo(newChangeAddressInfo, legacyChangeAddressInfo, i, true);
}

console.log('\n');
}

// Compare payment codes
console.log('Comparing payment codes:');
const newPaymentCode = newQiWallet.getPaymentCode(0);
const legacyPaymentCode = legacyQiWallet.getPaymentCode(0);
if (newPaymentCode === legacyPaymentCode) {
console.log('Payment codes match.');
} else {
console.log('Payment codes do not match:');
console.log('New wallet:', newPaymentCode);
console.log('Legacy wallet:', legacyPaymentCode);
}

// Compare getNextReceiveAddress
console.log('\nComparing getNextReceiveAddress:');
const bobNewPaymentCode = bobNewQiWallet.getPaymentCode(0);
const bobLegacyPaymentCode = bobLegacyQiWallet.getPaymentCode(0);

for (const zone of zones) {
console.log(`Comparing receive addresses for zone ${zone}:`);
for (let i = 0; i < addressCount; i++) {
const newReceiveAddress = await newQiWallet.getNextReceiveAddress(bobNewPaymentCode, zone);
const legacyReceiveAddress = await legacyQiWallet.getNextReceiveAddress(bobLegacyPaymentCode, zone);
compareAddressInfo(newReceiveAddress, legacyReceiveAddress, i, false, 'Receive');
}
}

// Compare getNextSendAddress
console.log('\nComparing getNextSendAddress:');
for (const zone of zones) {
console.log(`Comparing send addresses for zone ${zone}:`);
for (let i = 0; i < addressCount; i++) {
const newSendAddress = await newQiWallet.getNextSendAddress(bobNewPaymentCode, zone);
const legacySendAddress = await legacyQiWallet.getNextSendAddress(bobLegacyPaymentCode, zone);
compareAddressInfo(newSendAddress, legacySendAddress, i, false, 'Send');
}
}
}

function compareAddressInfo(newInfo, legacyInfo, index, isChange = false, addressType = '') {
const addressTypeString = addressType ? `${addressType} ` : '';
const changeString = isChange ? 'Change ' : '';
if (newInfo.address !== legacyInfo.address) {
console.log(`${changeString}${addressTypeString}Address #${index + 1} mismatch:`);
console.log('New wallet:', newInfo.address);
console.log('Legacy wallet:', legacyInfo.address);
} else if (newInfo.pubKey !== legacyInfo.pubKey) {
console.log(`${changeString}${addressTypeString}Address #${index + 1} public key mismatch:`);
console.log('New wallet:', newInfo.pubKey);
console.log('Legacy wallet:', legacyInfo.pubKey);
} else if (newInfo.index !== legacyInfo.index) {
console.log(`${changeString}${addressTypeString}Address #${index + 1} index mismatch:`);
console.log('New wallet:', newInfo.index);
console.log('Legacy wallet:', legacyInfo.index);
} else {
console.log(`${changeString}${addressTypeString}Address #${index + 1} matches.`);
}
}

compareWallets()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
93 changes: 35 additions & 58 deletions examples/wallets/qi-send.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ async function main() {
// Create Bob's wallet and connect to provider
const bobQiWallet = quais.QiHDWallet.fromMnemonic(bobMnemonic);
bobQiWallet.connect(provider);
const bobPaymentCode = await bobQiWallet.getPaymentCode(0);
const bobPaymentCode = bobQiWallet.getPaymentCode(0);

// Alice opens a channel to send Qi to Bob
aliceQiWallet.openChannel(bobPaymentCode, 'sender');
aliceQiWallet.openChannel(bobPaymentCode);

// Initialize Alice's wallet
console.log('Initializing Alice wallet...');
Expand All @@ -34,8 +34,8 @@ async function main() {
printWalletInfo(aliceQiWallet);

// Bob open channel with Alice
const alicePaymentCode = await aliceQiWallet.getPaymentCode(0);
bobQiWallet.openChannel(alicePaymentCode, 'receiver');
const alicePaymentCode = aliceQiWallet.getPaymentCode(0);
bobQiWallet.openChannel(alicePaymentCode);

// Bob initializes his wallet
console.log('Initializing Bob wallet...');
Expand All @@ -51,15 +51,12 @@ async function main() {
const tx = await aliceQiWallet.sendTransaction(bobPaymentCode, 1000, quais.Zone.Cyprus1, quais.Zone.Cyprus1);
console.log(`Tx contains ${tx.txInputs?.length} inputs`);
console.log(`Tx contains ${tx.txOutputs?.length} outputs`);
console.log('Tx: ', tx);
// wait for the transaction to be confirmed
// console.log('Tx: ', tx);
console.log('Waiting for transaction to be confirmed...');
// const receipt = await tx.wait(); //! throws 'wait() is not a function'
// console.log('Transaction confirmed: ', receipt);
// sleep for 5 seconds
await new Promise((resolve) => setTimeout(resolve, 5000));
// const receipt = await provider.getTransactionReceipt(tx.hash); //! throws 'invalid shard'
// console.log('Transaction confirmed: ', receipt);
// await tx.wait();

// sleep for 10 seconds to allow the transaction to be confirmed
await new Promise((resolve) => setTimeout(resolve, 15000));

// Bob syncs his wallet
console.log('Syncing Bob wallet...');
Expand Down Expand Up @@ -92,10 +89,8 @@ function printWalletInfo(wallet) {
'Change Addresses': serializedWallet.changeAddresses.length,
'Gap Addresses': serializedWallet.gapAddresses.length,
'Gap Change Addresses': serializedWallet.gapChangeAddresses.length,
'Used Gap Addresses': serializedWallet.usedGapAddresses.length,
'Used Gap Change Addresses': serializedWallet.usedGapChangeAddresses.length,
'Receiver PaymentCode addresses': Object.keys(wallet.receiverPaymentCodeInfo).length,
'Sender PaymentCode addresses': Object.keys(wallet.senderPaymentCodeInfo).length,
// 'Payment Channel Addresses': wallet.getPaymentChannelAddressesForZone(quais.Zone.Cyprus1).length,
// 'Sender PaymentCode addresses': Object.keys(wallet.senderPaymentCodeInfo).length,
'Available Outpoints': serializedWallet.outpoints.length,
'Pending Outpoints': serializedWallet.pendingOutpoints.length,
'Coin Type': serializedWallet.coinType,
Expand Down Expand Up @@ -141,24 +136,6 @@ function printWalletInfo(wallet) {
}));
console.table(gapChangeAddressesTable);

console.log('\nWallet Used Gap Addresses:');
const usedGapAddressesTable = serializedWallet.usedGapAddresses.map((addr) => ({
PubKey: addr.pubKey,
Address: addr.address,
Index: addr.index,
Zone: addr.zone,
}));
console.table(usedGapAddressesTable);

console.log('\nWallet Used Gap Change Addresses:');
const usedGapChangeAddressesTable = serializedWallet.usedGapChangeAddresses.map((addr) => ({
PubKey: addr.pubKey,
Address: addr.address,
Index: addr.index,
Zone: addr.zone,
}));
console.table(usedGapChangeAddressesTable);

console.log('\nWallet Outpoints:');
const outpointsInfoTable = serializedWallet.outpoints.map((outpoint) => ({
Address: outpoint.address,
Expand All @@ -182,30 +159,30 @@ function printWalletInfo(wallet) {
console.table(pendingOutpointsInfoTable);

// Print receiver payment code info
console.log('\nWallet Receiver Payment Code Info:');
const receiverPaymentCodeInfo = wallet.receiverPaymentCodeInfo;
for (const [paymentCode, addressInfoArray] of Object.entries(receiverPaymentCodeInfo)) {
console.log(`Payment Code: ${paymentCode}`);
const receiverTable = addressInfoArray.map((info) => ({
Address: info.address,
PubKey: info.pubKey,
Index: info.index,
Zone: info.zone,
}));
console.table(receiverTable);
}
// console.log('\nWallet Receiver Payment Code Info:');
// const openChannels = wallet.openChannels;
// for (const paymentCode of openChannels) {
// console.log(`Payment Code: ${paymentCode}`);
// const receiverTable = wallet.getPaymentChannelAddressesForZone(paymentCode, quais.Zone.Cyprus1).map((info) => ({
// Address: info.address,
// PubKey: info.pubKey,
// Index: info.index,
// Zone: info.zone,
// }));
// console.table(receiverTable);
// }

// Print sender payment code info
console.log('\nWallet Sender Payment Code Info:');
const senderPaymentCodeInfo = wallet.senderPaymentCodeInfo;
for (const [paymentCode, addressInfoArray] of Object.entries(senderPaymentCodeInfo)) {
console.log(`Payment Code: ${paymentCode}`);
const senderTable = addressInfoArray.map((info) => ({
Address: info.address,
PubKey: info.pubKey,
Index: info.index,
Zone: info.zone,
}));
console.table(senderTable);
}
// console.log('\nWallet Sender Payment Code Info:');
// const senderPaymentCodeInfo = wallet.senderPaymentCodeInfo;
// for (const [paymentCode, addressInfoArray] of Object.entries(senderPaymentCodeInfo)) {
// console.log(`Payment Code: ${paymentCode}`);
// const senderTable = addressInfoArray.map((info) => ({
// Address: info.address,
// PubKey: info.pubKey,
// Index: info.index,
// Zone: info.zone,
// }));
// console.table(senderTable);
// }
}
10 changes: 5 additions & 5 deletions src/_tests/unit/bip47.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,19 @@ describe('Test opening channels', function () {
it('opens a channel correctly', async function () {
const paymentCode =
'PM8TJTzqM3pqdQxBA52AX9M5JBCdkyJYWpNfJZpNX9H7FY2XitYFd99LSfCCQamCN5LubK1YNQMoz33g1WgVNX2keWoDtfDG9H1AfGcupRzHsPn6Rc2z';
bobQiWallet.openChannel(paymentCode, 'receiver');
assert.deepEqual(bobQiWallet.receiverPaymentCodeInfo[paymentCode], []);
bobQiWallet.openChannel(paymentCode);
assert.equal(bobQiWallet.channelIsOpen(paymentCode), true);
});

it('does nothing if the channel is already open', async function () {
const paymentCode =
'PM8TJTzqM3pqdQxBA52AX9M5JBCdkyJYWpNfJZpNX9H7FY2XitYFd99LSfCCQamCN5LubK1YNQMoz33g1WgVNX2keWoDtfDG9H1AfGcupRzHsPn6Rc2z';
bobQiWallet.openChannel(paymentCode, 'receiver');
assert.deepEqual(bobQiWallet.receiverPaymentCodeInfo[paymentCode], []);
bobQiWallet.openChannel(paymentCode);
assert.equal(bobQiWallet.channelIsOpen(paymentCode), true);
});

it('returns an error if the payment code is not valid', async function () {
const invalidPaymentCode = 'InvalidPaymentCode';
assert.throws(() => bobQiWallet.openChannel(invalidPaymentCode, 'receiver'), /Invalid payment code/);
assert.throws(() => bobQiWallet.openChannel(invalidPaymentCode), /Invalid payment code/);
});
});
15 changes: 5 additions & 10 deletions src/_tests/unit/qihdwallet.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,20 +122,15 @@ describe('QiHDWallet: Test serialization and deserialization of QiHDWallet with

// Assertions
assert.strictEqual(
await deserializedAliceWallet.getPaymentCode(0),
deserializedAliceWallet.getPaymentCode(0),
alicePaymentCode,
'Payment code should match after deserialization',
);

assert.deepStrictEqual(
deserializedAliceWallet.receiverPaymentCodeInfo,
aliceQiWallet.receiverPaymentCodeInfo,
'Receiver payment code info should match',
);
assert.deepStrictEqual(
deserializedAliceWallet.senderPaymentCodeInfo,
aliceQiWallet.senderPaymentCodeInfo,
'Sender payment code info should match',
assert.equal(
deserializedAliceWallet.channelIsOpen(alicePaymentCode),
aliceQiWallet.channelIsOpen(alicePaymentCode),
'Channel should be open',
);
});
});
Expand Down
40 changes: 37 additions & 3 deletions src/wallet/hdwallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export interface SerializedHDWallet {
version: number;
phrase: string;
coinType: AllowedCoinType;
addresses: Array<NeuteredAddressInfo>;
}

/**
Expand Down Expand Up @@ -441,12 +440,10 @@ export abstract class AbstractHDWallet {
* mnemonic phrase, coin type, and addresses.
*/
public serialize(): SerializedHDWallet {
const addresses = Array.from(this._addresses.values());
return {
version: (this.constructor as any)._version,
phrase: this._root.mnemonic!.phrase,
coinType: this.coinType(),
addresses: addresses,
};
}

Expand All @@ -463,6 +460,43 @@ export abstract class AbstractHDWallet {
throw new Error('deserialize method must be implemented in the subclass');
}

/**
* Validates the NeuteredAddressInfo object.
*
* @param {NeuteredAddressInfo} info - The NeuteredAddressInfo object to be validated.
* @throws {Error} If the NeuteredAddressInfo object is invalid.
* @protected
*/
protected validateNeuteredAddressInfo(info: NeuteredAddressInfo): void {
if (!/^(0x)?[0-9a-fA-F]{40}$/.test(info.address)) {
throw new Error(
`Invalid NeuteredAddressInfo: address must be a 40-character hexadecimal string: ${info.address}`,
);
}

if (!/^0x[0-9a-fA-F]{66}$/.test(info.pubKey)) {
throw new Error(
`Invalid NeuteredAddressInfo: pubKey must be a 32-character hexadecimal string with 0x prefix: ${info.pubKey}`,
);
}

if (!Number.isInteger(info.account) || info.account < 0) {
throw new Error(`Invalid NeuteredAddressInfo: account must be a non-negative integer: ${info.account}`);
}

if (!Number.isInteger(info.index) || info.index < 0) {
throw new Error(`Invalid NeuteredAddressInfo: index must be a non-negative integer: ${info.index}`);
}

if (typeof info.change !== 'boolean') {
throw new Error(`Invalid NeuteredAddressInfo: change must be a boolean: ${info.change}`);
}

if (!Object.values(Zone).includes(info.zone)) {
throw new Error(`Invalid NeuteredAddressInfo: zone '${info.zone}' is not a valid Zone`);
}
}

/**
* Validates the version and coinType of the serialized wallet.
*
Expand Down
Loading
Loading