diff --git a/examples/signing/pubkey-to-address.js b/examples/signing/pubkey-to-address.js new file mode 100644 index 00000000..f1c91e6f --- /dev/null +++ b/examples/signing/pubkey-to-address.js @@ -0,0 +1,37 @@ +const quais = require('../../lib/commonjs/quais'); + + +async function main() { + // Check if a public key is provided as a command line argument + if (process.argv.length < 3) { + console.error('Please provide a public key as a command line argument'); + process.exit(1); + } + + const pubkey = process.argv[2]; + + // Verify if the provided string is a valid public key of the type 0x0250495cb2f9535c684ebe4687b501c0d41a623d68c118b8dcecd393370f1d90e6 + if (!quais.isHexString(pubkey) || pubkey.length !== 68) { + console.error('Invalid public key format'); + process.exit(1); + } + + + try { + // Compute the address from the public key + const address = quais.computeAddress(pubkey); + console.log(`Public Key: ${pubkey}`); + console.log(`Derived Address: ${address}`); + } catch (error) { + console.error('Error computing address:', error.message); + process.exit(1); + } + +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/examples/wallets/qi-wallet-alice-bob-send-receive.js b/examples/wallets/qi-wallet-alice-bob-send-receive.js new file mode 100644 index 00000000..8489da2c --- /dev/null +++ b/examples/wallets/qi-wallet-alice-bob-send-receive.js @@ -0,0 +1,87 @@ +const quais = require('../../lib/commonjs/quais'); +const { printWalletInfo } = require('./utils'); +require('dotenv').config(); + +async function main() { + // Create provider + console.log('RPC URL: ', process.env.RPC_URL); + const provider = new quais.JsonRpcProvider(process.env.RPC_URL); + + // Create wallet and connect to provider + const mnemonic = quais.Mnemonic.fromPhrase(process.env.MNEMONIC); + const aliceQiWallet = quais.QiHDWallet.fromMnemonic(mnemonic); + const alicePaymentCode = aliceQiWallet.getPaymentCode(0); + aliceQiWallet.connect(provider); + + const bobMnemonic = quais.Mnemonic.fromPhrase("innocent perfect bus miss prevent night oval position aspect nut angle usage expose grace juice"); + const bobQiWallet = quais.QiHDWallet.fromMnemonic(bobMnemonic); + bobQiWallet.connect(provider); + const bobPaymentCode = bobQiWallet.getPaymentCode(0); + aliceQiWallet.openChannel(bobPaymentCode); + + console.log('Scanning Alice Qi wallet...'); + await aliceQiWallet.scan(quais.Zone.Cyprus1); + console.log('Alice Qi wallet scan complete'); + + printWalletInfo('Alice', aliceQiWallet); + + // Bob opens a channel with Alice + bobQiWallet.openChannel(alicePaymentCode); + console.log('Scanning Bob Qi wallet...'); + await bobQiWallet.scan(quais.Zone.Cyprus1); + console.log('Bob Qi wallet scan complete'); + printWalletInfo('Bob', bobQiWallet); + + // Alice sends 50 Qi to Bob + console.log('\nAlice sends 50 Qi to Bob'); + const tx = await aliceQiWallet.sendTransaction(bobPaymentCode, 50000n, quais.Zone.Cyprus1, quais.Zone.Cyprus1); + // console.log('Transaction sent: ', tx); + console.log(`Transaction hash: ${tx.hash}`); + console.log(`Tx contains ${tx.txInputs?.length} inputs`); + console.log(`Tx inputs: ${JSON.stringify(tx.txInputs)}`); + console.log(`Tx contains ${tx.txOutputs?.length} outputs`); + + console.log('Waiting for transaction to be confirmed...'); + const response = await tx.wait(); + console.log('Transaction confirmed in block: ', response.blockNumber); + + console.log('Syncing Alice Qi wallet...'); + await aliceQiWallet.sync(quais.Zone.Cyprus1); + console.log('Alice Qi wallet sync complete'); + + printWalletInfo('Alice', aliceQiWallet); + + console.log('Syncing Bob Qi wallet...'); + await bobQiWallet.sync(quais.Zone.Cyprus1); + console.log('Bob Qi wallet sync complete'); + printWalletInfo('Bob', bobQiWallet); + + console.log('\nBob sends back 25 Qi to Alice'); + const tx2 = await bobQiWallet.sendTransaction(alicePaymentCode, 25000n, quais.Zone.Cyprus1, quais.Zone.Cyprus1); + console.log(`Transaction hash: ${tx2.hash}`); + console.log(`Tx contains ${tx2.txInputs?.length} inputs`); + console.log(`Tx inputs: ${JSON.stringify(tx2.txInputs)}`); + console.log(`Tx contains ${tx2.txOutputs?.length} outputs`); + + console.log('Waiting for transaction to be confirmed...'); + const response2 = await tx2.wait(); + console.log('Transaction confirmed in block: ', response2.blockNumber); + + console.log('Syncing Alice Qi wallet...'); + await aliceQiWallet.sync(quais.Zone.Cyprus1); + console.log('Alice Qi wallet sync complete'); + + printWalletInfo('Alice', aliceQiWallet); + + console.log('Syncing Bob Qi wallet...'); + await bobQiWallet.sync(quais.Zone.Cyprus1); + console.log('Bob Qi wallet sync complete'); + printWalletInfo('Bob', bobQiWallet); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/examples/wallets/qi-wallet-convert-to-quai.js b/examples/wallets/qi-wallet-convert-to-quai.js new file mode 100644 index 00000000..94b0355f --- /dev/null +++ b/examples/wallets/qi-wallet-convert-to-quai.js @@ -0,0 +1,73 @@ +const quais = require('../../lib/commonjs/quais'); +const { printWalletInfo } = require('./utils'); +require('dotenv').config(); + +async function main() { + // Create provider + console.log('RPC URL: ', process.env.RPC_URL); + const provider = new quais.JsonRpcProvider(process.env.RPC_URL); + + // Create Alice's Qi wallet and connect to provider + const mnemonic = quais.Mnemonic.fromPhrase(process.env.MNEMONIC); + const aliceQiWallet = quais.QiHDWallet.fromMnemonic(mnemonic); + aliceQiWallet.connect(provider); + + // Initialize Alice's Qi wallet + console.log('\nInitializing Alice Qi wallet...'); + await aliceQiWallet.scan(quais.Zone.Cyprus1); + console.log('Alice Qi wallet scan complete'); + + printWalletInfo('Alice', aliceQiWallet); + + // Create Alice's Quai wallet and connect to provider + const aliceQuaiWallet = quais.QuaiHDWallet.fromMnemonic(mnemonic); + aliceQuaiWallet.connect(provider); + + // derive quai address + const quaiAddressInfo = await aliceQuaiWallet.getNextAddress(0, quais.Zone.Cyprus1); + console.log('\nAlice Quai address:', quaiAddressInfo.address); + + console.log('\nAlice converts 100 Qi to Quai...'); + + const tx = await aliceQiWallet.convertToQuai(quaiAddressInfo.address, 100000); + // console.log('Transaction sent: ', tx); + console.log(`Transaction hash: ${tx.hash}`); + console.log(`Tx contains ${tx.txInputs?.length} inputs`); + console.log(`Tx contains ${tx.txOutputs?.length} outputs`); + // wait for the transaction to be confirmed + console.log('Waiting for transaction to be confirmed...'); + const response = await tx.wait(); + console.log('Transaction confirmed in block: ', response.blockNumber); + + console.log('Syncing Alice Qi wallet...'); + await aliceQiWallet.sync(quais.Zone.Cyprus1); + console.log('Alice Qi wallet sync complete'); + + printWalletInfo('Alice', aliceQiWallet); + + // print Alice's Quai address balance + const balance = await provider.getBalance(quaiAddressInfo.address); + console.log('\nAlice Quai address balance:', quais.formatQuai(balance)); + + // repeat the same process of converting 100 Qi to Quai + console.log('Alice converts another 100 Qi to Quai...'); + const tx2 = await aliceQiWallet.convertToQuai(quaiAddressInfo.address, 100000); + console.log(`Tx contains ${tx2.txInputs?.length} inputs`); + console.log(`Tx contains ${tx2.txOutputs?.length} outputs`); + console.log('Waiting for transaction to be confirmed...'); + const response2 = await tx2.wait(); + console.log('Transaction confirmed in block: ', response2.blockNumber); + + console.log('Syncing Alice Qi wallet...'); + await aliceQiWallet.sync(quais.Zone.Cyprus1); + console.log('Alice Qi wallet sync complete'); + + printWalletInfo('Alice', aliceQiWallet); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/examples/wallets/qi-wallet-send-qi-to-bob.js b/examples/wallets/qi-wallet-send-qi-to-bob.js new file mode 100644 index 00000000..6581eb25 --- /dev/null +++ b/examples/wallets/qi-wallet-send-qi-to-bob.js @@ -0,0 +1,87 @@ +const quais = require('../../lib/commonjs/quais'); +const { printWalletInfo } = require('./utils'); +require('dotenv').config(); + +async function main() { + // Create provider + console.log('RPC URL: ', process.env.RPC_URL); + const provider = new quais.JsonRpcProvider(process.env.RPC_URL); + + // Create wallet and connect to provider + const mnemonic = quais.Mnemonic.fromPhrase(process.env.MNEMONIC); + const aliceQiWallet = quais.QiHDWallet.fromMnemonic(mnemonic); + const alicePaymentCode = aliceQiWallet.getPaymentCode(0); + aliceQiWallet.connect(provider); + + const bobMnemonic = quais.Mnemonic.fromPhrase("innocent perfect bus miss prevent night oval position aspect nut angle usage expose grace juice"); + const bobQiWallet = quais.QiHDWallet.fromMnemonic(bobMnemonic); + bobQiWallet.connect(provider); + const bobPaymentCode = bobQiWallet.getPaymentCode(0); + aliceQiWallet.openChannel(bobPaymentCode); + + console.log('Scanning Alice Qi wallet...'); + await aliceQiWallet.scan(quais.Zone.Cyprus1); + console.log('Alice Qi wallet scan complete'); + + printWalletInfo('Alice', aliceQiWallet); + + // Bob opens a channel with Alice + bobQiWallet.openChannel(alicePaymentCode); + console.log('Scanning Bob Qi wallet...'); + await bobQiWallet.scan(quais.Zone.Cyprus1); + console.log('Bob Qi wallet scan complete'); + printWalletInfo('Bob', bobQiWallet); + + // Alice sends 50 Qi to Bob + console.log('\nAlice sends 50 Qi to Bob'); + const tx = await aliceQiWallet.sendTransaction(bobPaymentCode, 50000n, quais.Zone.Cyprus1, quais.Zone.Cyprus1); + // console.log('Transaction sent: ', tx); + console.log(`Transaction hash: ${tx.hash}`); + console.log(`Tx contains ${tx.txInputs?.length} inputs`); + console.log(`Tx inputs: ${JSON.stringify(tx.txInputs)}`); + console.log(`Tx contains ${tx.txOutputs?.length} outputs`); + + console.log('Waiting for transaction to be confirmed...'); + const response = await tx.wait(); + console.log('Transaction confirmed in block: ', response.blockNumber); + + console.log('Syncing Alice Qi wallet...'); + await aliceQiWallet.sync(quais.Zone.Cyprus1); + console.log('Alice Qi wallet sync complete'); + + printWalletInfo('Alice', aliceQiWallet); + + console.log('Syncing Bob Qi wallet...'); + await bobQiWallet.sync(quais.Zone.Cyprus1); + console.log('Bob Qi wallet sync complete'); + printWalletInfo('Bob', bobQiWallet); + + console.log('\nAlice sends another 50 Qi to Bob'); + const tx2 = await aliceQiWallet.sendTransaction(bobPaymentCode, 50000n, quais.Zone.Cyprus1, quais.Zone.Cyprus1); + console.log(`Transaction hash: ${tx2.hash}`); + console.log(`Tx contains ${tx2.txInputs?.length} inputs`); + console.log(`Tx inputs: ${JSON.stringify(tx2.txInputs)}`); + console.log(`Tx contains ${tx2.txOutputs?.length} outputs`); + + console.log('Waiting for transaction to be confirmed...'); + const response2 = await tx2.wait(); + console.log('Transaction confirmed in block: ', response2.blockNumber); + + console.log('Syncing Alice Qi wallet...'); + await aliceQiWallet.sync(quais.Zone.Cyprus1); + console.log('Alice Qi wallet sync complete'); + + printWalletInfo('Alice', aliceQiWallet); + + console.log('Syncing Bob Qi wallet...'); + await bobQiWallet.sync(quais.Zone.Cyprus1); + console.log('Bob Qi wallet sync complete'); + printWalletInfo('Bob', bobQiWallet); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/examples/wallets/utils.js b/examples/wallets/utils.js new file mode 100644 index 00000000..3229874d --- /dev/null +++ b/examples/wallets/utils.js @@ -0,0 +1,132 @@ +const quais = require('../../lib/commonjs/quais'); +const printWalletInfo = (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:')); + }; + + 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 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); + + // Print BIP44 External Addresses + console.log(`\n${name} BIP44 External Addresses:`); + printAddressTable(getAddressesByType('BIP44:external')); + printUnusedAddressesData('BIP44:external'); + + // Print BIP44 Change Addresses + console.log(`\n${name} BIP44 Change Addresses:`); + printAddressTable(getAddressesByType('BIP44:change')); + printUnusedAddressesData('BIP44:change'); + + // Print BIP47 Addresses + console.log(`\n${name} BIP47 Addresses:`); + printAddressTable(getAddressesByType('BIP47')); + printUnusedAddressesData('BIP47'); + + // Print Outpoints + console.log(`\n${name} Wallet Outpoints:`); + printOutpointTable(serializedWallet.outpoints); + + // Print Pending Outpoints + // console.log(`\n${name} Wallet Pending Outpoints:`); + // printOutpointTable(serializedWallet.pendingOutpoints); + + // 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`); +} + +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); +} + +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); +} + +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); + } +} + + +module.exports = { + printWalletInfo, +}; diff --git a/src/wallet/payment-codes.ts b/src/wallet/payment-codes.ts index 2c48260b..e100c852 100644 --- a/src/wallet/payment-codes.ts +++ b/src/wallet/payment-codes.ts @@ -1,8 +1,7 @@ import { BIP32API, BIP32Interface, HDNodeBIP32Adapter } from './bip32/types.js'; import { sha256 } from '@noble/hashes/sha256'; -import { keccak256 } from '../crypto/index.js'; import { getBytes, hexlify } from '../utils/data.js'; -import { getAddress } from '../address/address.js'; +import { computeAddress } from '../address/address.js'; import { bs58check } from './bip32/crypto.js'; import type { TinySecp256k1Interface } from './bip32/types.js'; import { secp256k1 } from '@noble/curves/secp256k1'; @@ -159,7 +158,7 @@ export class PaymentCodePublic { * @protected */ protected getAddressFromPubkey(pubKey: Uint8Array): string { - return getAddress(keccak256('0x' + hexlify(pubKey).substring(4)).substring(26)); + return computeAddress(hexlify(pubKey)); } /** @@ -171,9 +170,8 @@ export class PaymentCodePublic { * @throws {Error} - If unable to derive public key or if an unknown address type is specified. */ getPaymentAddress(paymentCode: PaymentCodePrivate, idx: number): string { - const pubkey = hexlify(this.derivePaymentPublicKey(paymentCode, idx)); - - return getAddress(keccak256('0x' + pubkey.substring(4)).substring(26)); + const pubkey = this.derivePaymentPublicKey(paymentCode, idx); + return this.getAddressFromPubkey(pubkey); } } diff --git a/src/wallet/qi-hdwallet.ts b/src/wallet/qi-hdwallet.ts index e1b1c834..1f10c232 100644 --- a/src/wallet/qi-hdwallet.ts +++ b/src/wallet/qi-hdwallet.ts @@ -218,7 +218,7 @@ export class QiHDWallet extends AbstractHDWallet { } /** - * Finds the last used index in an array of QiAddressInfo objects. + * Finds the last used index in an array of QiAddressInfo objects. If no index is found, returns -1. * * @param {QiAddressInfo[]} addresses - The array of QiAddressInfo objects. * @returns {number} The last used index. @@ -227,7 +227,7 @@ export class QiHDWallet extends AbstractHDWallet { const filteredAddresses = addresses?.filter( (addressInfo) => addressInfo.account === account && addressInfo.zone === zone, ); - return filteredAddresses?.reduce((maxIndex, addressInfo) => Math.max(maxIndex, addressInfo.index), -1) || 0; + return filteredAddresses?.reduce((maxIndex, addressInfo) => Math.max(maxIndex, addressInfo.index), -1) || -1; } /** @@ -1353,11 +1353,11 @@ export class QiHDWallet extends AbstractHDWallet { const paymentCodeInfoArray = this._paymentCodeSendAddressMap.get(receiverPaymentCode); const lastIndex = this._findLastUsedIndex(paymentCodeInfoArray, account, zone); - let addrIndex = lastIndex; + let addrIndex = lastIndex + 1; for (let attempts = 0; attempts < MAX_ADDRESS_DERIVATION_ATTEMPTS; attempts++) { - const address = receiverPCodePublic.getPaymentAddress(walletPCodePrivate, addrIndex++); + const address = receiverPCodePublic.getPaymentAddress(walletPCodePrivate, addrIndex); if (this.isValidAddressForZone(address, zone)) { - const pubkey = receiverPCodePublic.derivePaymentPublicKey(walletPCodePrivate, addrIndex - 1); + const pubkey = receiverPCodePublic.derivePaymentPublicKey(walletPCodePrivate, addrIndex); const pcInfo: QiAddressInfo = { address, pubKey: hexlify(pubkey), @@ -1375,6 +1375,7 @@ export class QiHDWallet extends AbstractHDWallet { } return pcInfo; } + addrIndex++; } throw new Error( @@ -1402,11 +1403,11 @@ export class QiHDWallet extends AbstractHDWallet { const paymentCodeInfoArray = this._addressesMap.get(senderPaymentCode); const lastIndex = this._findLastUsedIndex(paymentCodeInfoArray, account, zone); - let addrIndex = lastIndex; + let addrIndex = lastIndex + 1; for (let attempts = 0; attempts < MAX_ADDRESS_DERIVATION_ATTEMPTS; attempts++) { - const address = walletPCodePrivate.getPaymentAddress(senderPCodePublic, addrIndex++); + const address = walletPCodePrivate.getPaymentAddress(senderPCodePublic, addrIndex); if (this.isValidAddressForZone(address, zone)) { - const pubkey = walletPCodePrivate.derivePaymentPublicKey(senderPCodePublic, addrIndex - 1); + const pubkey = walletPCodePrivate.derivePaymentPublicKey(senderPCodePublic, addrIndex); const pcInfo: QiAddressInfo = { address, pubKey: hexlify(pubkey), @@ -1424,6 +1425,7 @@ export class QiHDWallet extends AbstractHDWallet { } return pcInfo; } + addrIndex++; } throw new Error(