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

Overhaul Qi wallet syncing logic #366

Merged
merged 10 commits into from
Nov 22, 2024
201 changes: 89 additions & 112 deletions examples/wallets/utils.js
Original file line number Diff line number Diff line change
@@ -1,135 +1,112 @@
const quais = require('../../lib/commonjs/quais');
const printWalletInfo = (name, wallet) => {
const serializedWallet = wallet.serialize();
const printWalletInfo = async (name, wallet) => {
const serializedWallet = wallet.serialize();

// Helper function to get addresses by type and status
const getAddressesByType = (type) => {
if (type == 'BIP44:external' || type == 'BIP44:change') {
return serializedWallet.addresses
.filter(addr => addr.derivationPath === type);
}
return serializedWallet.addresses
.filter(addr => !addr.derivationPath.startsWith('BIP44:'));
};
// Helper function to get addresses by type and status
const getAddressesByType = (type) => {
if (type == 'BIP44:external' || type == 'BIP44:change') {
return serializedWallet.addresses.filter((addr) => addr.derivationPath === type);
}
return serializedWallet.addresses.filter((addr) => !addr.derivationPath.startsWith('BIP44:'));
};

const printUnusedAddressesData = (type) => {
const addresses = getAddressesByType(type);

// Find the index where the last group of UNUSED addresses starts
let lastUnusedGroupStartIndex = addresses.length;
for (let i = addresses.length - 1; i >= 0; i--) {
if (addresses[i].status !== 'UNUSED') {
break;
}
lastUnusedGroupStartIndex = i;
}

// Filter addresses: UNUSED and not part of the last group
const filteredUnusedAddresses = addresses.filter((addr, index) =>
addr.status === 'UNUSED' && index < lastUnusedGroupStartIndex
);

if (filteredUnusedAddresses.length > 0) {
const outpoints = serializedWallet.outpoints;
for (const addr of filteredUnusedAddresses) {
const outpoint = outpoints.find(outpoint => outpoint.address === addr.address);
if (outpoint) {
console.log(`\tOutpoint for UNUSED address ${addr.address}: ${JSON.stringify(outpoint)}`);
} else {
console.log(`\tOutpoint for UNUSED address ${addr.address} not found`);
}
}
}
};
const printUnusedAddressesData = (type) => {
const addresses = getAddressesByType(type);

const summary = {
'BIP44 External Addresses': getAddressesByType('BIP44:external').length,
'BIP44 Change Addresses': getAddressesByType('BIP44:change').length,
'BIP47 Addresses': getAddressesByType('BIP47').length,
'Available Outpoints': serializedWallet.outpoints.length,
'Pending Outpoints': serializedWallet.pendingOutpoints.length,
'Sender Payment Code Info': Object.keys(serializedWallet.senderPaymentCodeInfo).length,
'Coin Type': serializedWallet.coinType,
'Version': serializedWallet.version
};
console.log(`\n**************************************************** ${name} Qi wallet summary: ************************************************\n`);
console.table(summary);
// Find the index where the last group of UNUSED addresses starts
let lastUnusedGroupStartIndex = addresses.length;
for (let i = addresses.length - 1; i >= 0; i--) {
if (addresses[i].status !== 'UNUSED') {
break;
}
lastUnusedGroupStartIndex = i;
}

// Print BIP44 External Addresses
console.log(`\n${name} BIP44 External Addresses:`);
printAddressTable(getAddressesByType('BIP44:external'));
printUnusedAddressesData('BIP44:external');
// Filter addresses: UNUSED and not part of the last group
const filteredUnusedAddresses = addresses.filter(
(addr, index) => addr.status === 'UNUSED' && index < lastUnusedGroupStartIndex,
);
};

// Print BIP44 Change Addresses
console.log(`\n${name} BIP44 Change Addresses:`);
printAddressTable(getAddressesByType('BIP44:change'));
printUnusedAddressesData('BIP44:change');
const summary = {
'BIP44 External Addresses': getAddressesByType('BIP44:external').length,
'BIP44 Change Addresses': getAddressesByType('BIP44:change').length,
'BIP47 Addresses': getAddressesByType('BIP47').length,
'Sender Payment Code Info': Object.keys(serializedWallet.senderPaymentCodeInfo).length,
'Coin Type': serializedWallet.coinType,
Version: serializedWallet.version,
};
console.log(
`\n**************************************************** ${name} Qi wallet summary: ************************************************\n`,
);
console.table(summary);

// Print BIP47 Addresses
console.log(`\n${name} BIP47 Addresses:`);
printAddressTable(getAddressesByType('BIP47'));
printUnusedAddressesData('BIP47');
// Print BIP44 External Addresses
console.log(`\n${name} BIP44 External Addresses:`);
printAddressTable(getAddressesByType('BIP44:external'));
printUnusedAddressesData('BIP44:external');

// Print Outpoints
console.log(`\n${name} Wallet Outpoints:`);
printOutpointTable(serializedWallet.outpoints);
// Print BIP44 Change Addresses
console.log(`\n${name} BIP44 Change Addresses:`);
printAddressTable(getAddressesByType('BIP44:change'));
printUnusedAddressesData('BIP44:change');

// Print Pending Outpoints
// console.log(`\n${name} Wallet Pending Outpoints:`);
// printOutpointTable(serializedWallet.pendingOutpoints);
// Print BIP47 Addresses
console.log(`\n${name} BIP47 Addresses:`);
printAddressTable(getAddressesByType('BIP47'));
printUnusedAddressesData('BIP47');

// Print Sender Payment Code Info
console.log(`\n${name} Wallet Sender Payment Code Info:`);
printPaymentCodeInfo(serializedWallet.senderPaymentCodeInfo);
// Print Sender Payment Code Info
console.log(`\n${name} Wallet Sender Payment Code Info:`);
printPaymentCodeInfo(serializedWallet.senderPaymentCodeInfo);

// Print wallet Qi balance
const walletBalance = wallet.getBalanceForZone(quais.Zone.Cyprus1);
console.log(`\n=> ${name} Wallet balance: ${quais.formatQi(walletBalance)} Qi\n`);
}
// Print wallet Qi balance
const walletBalance = await wallet.getBalanceForZone(quais.Zone.Cyprus1);
console.log(`\n=> ${name} Wallet balance: ${quais.formatQi(walletBalance)} Qi\n`);
};

function printAddressTable(addresses) {
const addressTable = addresses.map(addr => ({
PubKey: addr.pubKey,
Address: addr.address,
Index: addr.index,
Change: addr.change ? 'Yes' : 'No',
Zone: addr.zone,
Status: addr.status,
DerivationPath: addr.derivationPath
}));
console.table(addressTable);
const addressTable = addresses.map((addr) => ({
PubKey: addr.pubKey,
Address: addr.address,
Index: addr.index,
Change: addr.change ? 'Yes' : 'No',
Zone: addr.zone,
Status: addr.status,
DerivationPath: addr.derivationPath,
}));
console.table(addressTable);
}

function printOutpointTable(outpoints) {
const outpointTable = outpoints.map(outpoint => ({
Address: outpoint.address,
Denomination: outpoint.outpoint.denomination,
Index: outpoint.outpoint.index,
TxHash: outpoint.outpoint.txhash,
Zone: outpoint.zone,
Account: outpoint.account,
}));
console.table(outpointTable);
const outpointTable = outpoints.map((outpoint) => ({
Address: outpoint.address,
Denomination: outpoint.outpoint.denomination,
Index: outpoint.outpoint.index,
TxHash: outpoint.outpoint.txhash,
Zone: outpoint.zone,
Account: outpoint.account,
}));
console.table(outpointTable);
}

function printPaymentCodeInfo(paymentCodeInfo) {
for (const [paymentCode, addressInfoArray] of Object.entries(paymentCodeInfo)) {
console.log(`Payment Code: ${paymentCode}`);
const paymentCodeTable = addressInfoArray.map(info => ({
Address: info.address,
PubKey: info.pubKey,
Index: info.index,
Zone: info.zone,
Status: info.status
}));
console.table(paymentCodeTable);
}
for (const [paymentCode, addressInfoArray] of Object.entries(paymentCodeInfo)) {
console.log(`Payment Code: ${paymentCode}`);
const paymentCodeTable = addressInfoArray.map((info) => ({
Address: info.address,
PubKey: info.pubKey,
Index: info.index,
Zone: info.zone,
Status: info.status,
}));
console.table(paymentCodeTable);
}
}


module.exports = {
printWalletInfo,
printAddressTable,
printOutpointTable,
printPaymentCodeInfo
printWalletInfo,
printAddressTable,
printOutpointTable,
printPaymentCodeInfo,
};
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe('QiHDWallet Roundtrip Transaction', function () {
);

// Alice's balance should be lower than the initial balance minus the amount sent (because of the tx fee)
const aliceBalance = aliceWallet.getBalanceForZone(Zone.Cyprus1);
const aliceBalance = await aliceWallet.getBalanceForZone(Zone.Cyprus1);
const aliceBalanceWithoutFee = BigInt(test.alice.initialState.balance) - BigInt(test.alice.sendAmount);
aliceFee = BigInt(aliceBalanceWithoutFee) - BigInt(aliceBalance);
assert.ok(
Expand All @@ -156,8 +156,8 @@ describe('QiHDWallet Roundtrip Transaction', function () {
await aliceWallet.sync(Zone.Cyprus1);
await bobWallet.sync(Zone.Cyprus1);

const aliceBalance = aliceWallet.getBalanceForZone(Zone.Cyprus1);
const bobBalance = bobWallet.getBalanceForZone(Zone.Cyprus1);
const aliceBalance = await aliceWallet.getBalanceForZone(Zone.Cyprus1);
const bobBalance = await bobWallet.getBalanceForZone(Zone.Cyprus1);

const bobBalanceWithoutFee =
BigInt(test.bob.initialState.balance) + BigInt(test.alice.sendAmount) - BigInt(test.bob.sendAmount);
Expand Down
18 changes: 0 additions & 18 deletions src/_tests/unit/qihdwallet-serialization.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,6 @@ describe('QiHDWallet Serialization/Deserialization', function () {
assert.strictEqual(serializedWallet.phrase, test.phrase, 'Phrase mismatch');
assert.strictEqual(serializedWallet.coinType, test.coinType, 'Coin type mismatch');

// Compare outpoints
assert.deepStrictEqual(serializedWallet.outpoints, test.outpoints, 'Outpoints mismatch');

// Compare pending outpoints
assert.deepStrictEqual(
serializedWallet.pendingOutpoints,
test.pendingOutpoints,
'Pending outpoints mismatch',
);

// Compare addresses
assert.deepStrictEqual(
serializedWallet.addresses.sort((a, b) => a.index - b.index),
Expand Down Expand Up @@ -84,14 +74,6 @@ describe('QiHDWallet Serialization/Deserialization', function () {
'Gap addresses count mismatch',
);

// Verify outpoints were correctly imported
const zoneOutpoints = deserializedWallet.getOutpoints(zone);
assert.strictEqual(
zoneOutpoints.length,
test.outpoints.filter((outpoint) => outpoint.zone === zone).length,
'Outpoints count mismatch',
);

// Verify payment channels were correctly restored
const paymentCodes = Object.keys(test.senderPaymentCodeInfo);
for (const paymentCode of paymentCodes) {
Expand Down
19 changes: 16 additions & 3 deletions src/providers/abstract-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,11 @@ export type PerformActionRequest =
blockTag: BlockTag;
zone: Zone;
}
| {
method: 'getLockedBalance';
address: string;
zone: Zone;
}
| {
method: 'getOutpointsByAddress';
address: string;
Expand Down Expand Up @@ -678,7 +683,7 @@ export type PerformActionRequest =

type _PerformAccountRequest =
| {
method: 'getBalance' | 'getTransactionCount' | 'getCode' | 'getOutpointsByAddress';
method: 'getBalance' | 'getLockedBalance' | 'getTransactionCount' | 'getCode' | 'getOutpointsByAddress';
}
| {
method: 'getStorage';
Expand Down Expand Up @@ -1649,6 +1654,10 @@ export class AbstractProvider<C = FetchRequest> implements Provider {
return getBigInt(await this.#getAccountValue({ method: 'getBalance' }, address, blockTag), '%response');
}

async getLockedBalance(address: AddressLike): Promise<bigint> {
return getBigInt(await this.#getAccountValue({ method: 'getLockedBalance' }, address), '%response');
}

async getOutpointsByAddress(address: AddressLike): Promise<Outpoint[]> {
return formatOutpoints(await this.#getAccountValue({ method: 'getOutpointsByAddress' }, address, 'latest'));
}
Expand Down Expand Up @@ -1822,15 +1831,19 @@ export class AbstractProvider<C = FetchRequest> implements Provider {
return hexlify(result);
}

async getOutpointDeltas(addresses: string[], startHash: string, endHash: string): Promise<OutpointDeltas> {
async getOutpointDeltas(addresses: string[], startHash: string, endHash?: string): Promise<OutpointDeltas> {
// Validate addresses are Qi addresses
for (const addr of addresses) {
assertArgument(isQiAddress(addr), `Invalid Qi address: ${addr}`, 'addresses', addresses);
}

// Validate block hashes
assertArgument(isHexString(startHash, 32), 'invalid startHash', 'startHash', startHash);
assertArgument(isHexString(endHash, 32), 'invalid endHash', 'endHash', endHash);
if (endHash) {
assertArgument(isHexString(endHash, 32), 'invalid endHash', 'endHash', endHash);
} else {
endHash = 'latest';
}

// Get the zone from the first address
const zone = await this.zoneFromAddress(addresses[0]);
Expand Down
2 changes: 1 addition & 1 deletion src/providers/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export interface UncleParams {
location: string;
mixHash: string;
nonce: string;
number: string;
number: number;
parentHash: string;
timestamp: string;
txHash: string;
Expand Down
6 changes: 6 additions & 0 deletions src/providers/provider-jsonrpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,12 @@ export abstract class JsonRpcApiProvider<C = FetchRequest> extends AbstractProvi
args: [req.address, req.blockTag],
};

case 'getLockedBalance':
return {
method: 'quai_getLockedBalance',
args: [req.address],
};

case 'getOutpointsByAddress':
return {
method: 'quai_getOutpointsByAddress',
Expand Down
14 changes: 11 additions & 3 deletions src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ export class Uncle implements UncleParams {
readonly location!: string;
readonly mixHash!: string;
readonly nonce!: string;
readonly number!: string;
readonly number!: number;
readonly parentHash!: string;
readonly timestamp!: string;
readonly txHash!: string;
Expand Down Expand Up @@ -960,7 +960,7 @@ export class Block implements BlockParams, Iterable<string> {
}
return createOrphanedBlockFilter({
hash: this.hash!,
number: parseInt(this.woHeader.number!, 16),
number: this.woHeader.number!,
});
}
}
Expand Down Expand Up @@ -2805,6 +2805,14 @@ export interface Provider extends ContractRunner, EventEmitterable<ProviderEvent
*/
getBalance(address: AddressLike, blockTag?: BlockTag): Promise<bigint>;

/**
* Get the locked balance for `address`.
*
* @param {AddressLike} address - The address to fetch the locked balance for.
* @returns {Promise<bigint>} A promise resolving to the locked balance.
*/
getLockedBalance(address: AddressLike): Promise<bigint>;

/**
* Get the UTXO entries for `address`.
*
Expand Down Expand Up @@ -3036,5 +3044,5 @@ export interface Provider extends ContractRunner, EventEmitterable<ProviderEvent
*/
getLatestQuaiRate(zone: Zone, amt: bigint): Promise<bigint>;

getOutpointDeltas(addresses: string[], startHash: string, endHash: string): Promise<OutpointDeltas>;
getOutpointDeltas(addresses: string[], startHash: string, endHash?: string): Promise<OutpointDeltas>;
}
Loading
Loading