Skip to content

Commit

Permalink
add paymentcode info to serialization/deserialization
Browse files Browse the repository at this point in the history
  • Loading branch information
alejoacosta74 committed Sep 19, 2024
1 parent 1ce99ae commit 717ee1f
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 1 deletion.
54 changes: 54 additions & 0 deletions src/wallet/payment-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getAddress } from '../address/address.js';
import { bs58check } from './bip32/crypto.js';
import { HDNodeBIP32Adapter } from './bip32/types.js';
import type { TinySecp256k1Interface, BIP32API, BIP32Interface } from './bip32/types.js';
import { secp256k1 } from '@noble/curves/secp256k1';

export const PC_VERSION = 0x47;

Expand Down Expand Up @@ -277,3 +278,56 @@ export class PaymentCodePrivate extends PaymentCodePublic {
return child.privateKey!;
}
}

/**
* Validates a payment code base58 encoded string.
*
* @param {string} paymentCode - The payment code to validate.
* @throws {Error} If the payment code is invalid.
*/
export async function validatePaymentCode(paymentCode: string): Promise<boolean> {
try {
const decoded = bs58check.decode(paymentCode);

if (decoded.length !== 82) {
return false;
}

if (decoded[0] !== 0x47) {
return false;
}

const payload = decoded.slice(0, -4);
const checksum = decoded.slice(-4);
const calculatedChecksum = sha256(sha256(payload)).slice(0, 4);
if (!checksum.every((b, i) => b === calculatedChecksum[i])) {
return false;
}

const paymentCodeBytes = decoded.slice(1, -4);

if (paymentCodeBytes[0] !== 0x01 && paymentCodeBytes[0] !== 0x02) {
return false;
}
if (paymentCodeBytes[2] !== 0x02 && paymentCodeBytes[2] !== 0x03) {
return false;
}

const xCoordinate = paymentCodeBytes.slice(3, 35);
try {
secp256k1.ProjectivePoint.fromHex(xCoordinate).assertValidity();
} catch (error) {
console.log('error validating paymentcode x-coordinate: ', error);
return false;
}

if (!paymentCodeBytes.slice(67).every((byte) => byte === 0)) {
return false;
}

return true;
} catch (error) {
console.log('error validating paymentcode: ', error);
return false;
}
}
68 changes: 67 additions & 1 deletion src/wallet/qi-hdwallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Outpoint } from '../transaction/utxo.js';
import { getZoneForAddress } from '../utils/index.js';
import { AllowedCoinType, Zone } from '../constants/index.js';
import { Mnemonic } from './mnemonic.js';
import { PaymentCodePrivate, PaymentCodePublic, PC_VERSION } from './payment-codes.js';
import { PaymentCodePrivate, PaymentCodePublic, PC_VERSION, validatePaymentCode } from './payment-codes.js';
import { BIP32Factory } from './bip32/bip32.js';
import { bs58check } from './bip32/crypto.js';
import { type BIP32API, HDNodeBIP32Adapter } from './bip32/types.js';
Expand Down Expand Up @@ -59,6 +59,8 @@ interface SerializedQiHDWallet extends SerializedHDWallet {
changeAddresses: NeuteredAddressInfo[];
gapAddresses: NeuteredAddressInfo[];
gapChangeAddresses: NeuteredAddressInfo[];
receiverPaymentCodeInfo: { [key: string]: paymentCodeInfo[] };
senderPaymentCodeInfo: { [key: string]: paymentCodeInfo[] };
}

/**
Expand Down Expand Up @@ -158,6 +160,15 @@ export class QiHDWallet extends AbstractHDWallet {
super(guard, root, provider);
}

// getters for the payment code info maps
public get receiverPaymentCodeInfo(): { [key: string]: paymentCodeInfo[] } {
return Object.fromEntries(this._receiverPaymentCodeInfo);
}

public get senderPaymentCodeInfo(): { [key: string]: paymentCodeInfo[] } {
return Object.fromEntries(this._senderPaymentCodeInfo);
}

/**
* Promise that resolves to the next change address for the specified account and zone.
*
Expand Down Expand Up @@ -546,6 +557,8 @@ export class QiHDWallet extends AbstractHDWallet {
changeAddresses: Array.from(this._changeAddresses.values()),
gapAddresses: this._gapAddresses,
gapChangeAddresses: this._gapChangeAddresses,
receiverPaymentCodeInfo: Object.fromEntries(this._receiverPaymentCodeInfo),
senderPaymentCodeInfo: Object.fromEntries(this._senderPaymentCodeInfo),
...hdwalletSerialized,
};
}
Expand Down Expand Up @@ -591,9 +604,62 @@ export class QiHDWallet extends AbstractHDWallet {
// validate the outpoints and import them
wallet.validateOutpointInfo(serialized.outpoints);
wallet._outpoints.push(...serialized.outpoints);

// validate and import the payment code info
wallet.validateAndImportPaymentCodeInfo(serialized.receiverPaymentCodeInfo, 'receiver');
wallet.validateAndImportPaymentCodeInfo(serialized.senderPaymentCodeInfo, 'sender');

return wallet;
}

/**
* Validates and imports a map of payment code info.
*
* @param {Map<string, paymentCodeInfo[]>} paymentCodeInfoMap - The map of payment code info to validate and import.
* @param {'receiver' | 'sender'} target - The target map to update ('receiver' or 'sender').
* @throws {Error} If any of the payment code info is invalid.
*/
private validateAndImportPaymentCodeInfo(
paymentCodeInfoMap: { [key: string]: paymentCodeInfo[] },
target: 'receiver' | 'sender',
): void {
const targetMap = target === 'receiver' ? this._receiverPaymentCodeInfo : this._senderPaymentCodeInfo;

for (const [paymentCode, paymentCodeInfoArray] of Object.entries(paymentCodeInfoMap)) {
if (!validatePaymentCode(paymentCode)) {
throw new Error(`Invalid payment code: ${paymentCode}`);
}
for (const pcInfo of paymentCodeInfoArray) {
this.validatePaymentCodeInfo(pcInfo);
}
targetMap.set(paymentCode, paymentCodeInfoArray);
}
}

/**
* Validates a payment code info object.
*
* @param {paymentCodeInfo} pcInfo - The payment code info to validate.
* @throws {Error} If the payment code info is invalid.
*/
private validatePaymentCodeInfo(pcInfo: paymentCodeInfo): void {
if (!/^(0x)?[0-9a-fA-F]{40}$/.test(pcInfo.address)) {
throw new Error('Invalid payment code info: address must be a 40-character hexadecimal string');
}
if (!Number.isInteger(pcInfo.index) || pcInfo.index < 0) {
throw new Error('Invalid payment code info: index must be a non-negative integer');
}
if (typeof pcInfo.isUsed !== 'boolean') {
throw new Error('Invalid payment code info: isUsed must be a boolean');
}
if (!Object.values(Zone).includes(pcInfo.zone)) {
throw new Error(`Invalid payment code info: zone '${pcInfo.zone}' is not a valid Zone`);
}
if (!Number.isInteger(pcInfo.account) || pcInfo.account < 0) {
throw new Error('Invalid payment code info: account must be a non-negative integer');
}
}

/**
* Validates an array of OutpointInfo objects. This method checks the validity of each OutpointInfo object by
* performing the following validations:
Expand Down

0 comments on commit 717ee1f

Please sign in to comment.