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

make from not required so static contract calls work #193

Merged
merged 2 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 5 additions & 15 deletions src/contract/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ function buildWrappedMethod<

return await resolveProperties({
to: contract.getAddress(),
from: args.pop()?.from ?? '0x0000000000000000000000000000000000000000',
from: args.pop()?.from,
data: contract.interface.encodeFunctionData(fragment, resolvedArgs),
});
};
Expand All @@ -365,13 +365,8 @@ function buildWrappedMethod<
operation: 'sendTransaction',
});
const pop = await populateTransaction(...args);
if (
runner &&
'address' in runner &&
typeof runner.address == 'string' &&
pop.from === '0x0000000000000000000000000000000000000000'
) {
pop.from = runner.address;
if (!pop.from && 'address' in runner && typeof runner.address === 'string') {
pop.from = await resolveAddress(runner.address);
}

const tx = (await runner.sendTransaction(await pop)) as QuaiTransactionResponse;
Expand All @@ -396,13 +391,8 @@ function buildWrappedMethod<
operation: 'call',
});
const tx = await populateTransaction(...args);
if (
runner &&
'address' in runner &&
typeof runner.address == 'string' &&
tx.from === '0x0000000000000000000000000000000000000000'
) {
tx.from = runner.address;
if (!tx.from && 'address' in runner && typeof runner.address === 'string') {
tx.from = await resolveAddress(runner.address);
}

let result = '0x';
Expand Down
5 changes: 4 additions & 1 deletion src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,10 @@ export function addressFromTransactionRequest(tx: TransactionRequest): AddressLi
keccak256('0x' + SigningKey.computePublicKey(tx.inputs[0].pub_key).substring(4)).substring(26),
);
}
throw new Error('Unable to determine sender from transaction inputs or from field');
if ('to' in tx && tx.to !== null) {
return tx.to as AddressLike;
}
throw new Error('Unable to determine address from transaction inputs, from or to field');
}
/**
* A **TransactionRequest** is a transactions with potentially various properties not defined, or with less strict types
Expand Down
20 changes: 14 additions & 6 deletions src/transaction/quai-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface QuaiTransactionLike extends TransactionLike {
/**
* The sender.
*/
from: string;
from?: string;
/**
* The nonce.
*/
Expand Down Expand Up @@ -107,7 +107,7 @@ export class QuaiTransaction extends AbstractTransaction<Signature> implements Q
#maxFeePerGas: null | bigint;
#value: bigint;
#accessList: null | AccessList;
from: string;
from?: string;

/**
* The `to` address for the transaction or `null` if the transaction is an `init` transaction.
Expand All @@ -130,8 +130,11 @@ export class QuaiTransaction extends AbstractTransaction<Signature> implements Q
if (!this.originZone) {
throw new Error('Invalid Zone for from address');
}
if (!(this.from && this.to)) {
throw new Error('Missing from or to address');
}

const isSameLedger = isQiAddress(this.from) === isQiAddress(this.to || '');
const isSameLedger = isQiAddress(this.from) === isQiAddress(this.to);
if (this.isExternal && !isSameLedger) {
throw new Error('Cross-zone & cross-ledger transactions are not supported');
}
Expand All @@ -155,7 +158,7 @@ export class QuaiTransaction extends AbstractTransaction<Signature> implements Q
* The zone of the sender address
*/
get originZone(): Zone | undefined {
const zone = getZoneForAddress(this.from);
const zone = this.from ? getZoneForAddress(this.from) : undefined;
return zone ?? undefined;
}

Expand Down Expand Up @@ -268,7 +271,7 @@ export class QuaiTransaction extends AbstractTransaction<Signature> implements Q
/**
* Creates a new Transaction with default values.
*/
constructor(from: string) {
constructor(from?: string) {
super();
this.#to = null;
this.#nonce = 0;
Expand Down Expand Up @@ -441,7 +444,12 @@ export class QuaiTransaction extends AbstractTransaction<Signature> implements Q

if (tx.from != null) {
validateAddress(tx.from);
assertArgument(result.from.toLowerCase() === (tx.from || '').toLowerCase(), 'from mismatch', 'tx', tx);
assertArgument(
(result.from || '').toLowerCase() === (tx.from || '').toLowerCase(),
'from mismatch',
'tx',
tx,
);
result.from = tx.from;
}
return result;
Expand Down
161 changes: 94 additions & 67 deletions src/wallet/hdwallet.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { HDNodeWallet } from "./hdnodewallet";
import { Mnemonic } from "./mnemonic.js";
import { LangEn } from "../wordlists/lang-en.js"
import type { Wordlist } from "../wordlists/index.js";
import { randomBytes } from "../crypto/index.js";
import { getZoneForAddress, isQiAddress } from "../utils/index.js";
import { HDNodeWallet } from './hdnodewallet.js';
import { Mnemonic } from './mnemonic.js';
import { LangEn } from '../wordlists/lang-en.js';
import type { Wordlist } from '../wordlists/index.js';
import { randomBytes } from '../crypto/index.js';
import { getZoneForAddress, isQiAddress } from '../utils/index.js';
import { Zone } from '../constants/index.js';
import { TransactionRequest, Provider, TransactionResponse } from '../providers/index.js';
import { AllowedCoinType } from "../constants/index.js";
import { AllowedCoinType } from '../constants/index.js';

export interface NeuteredAddressInfo {
pubKey: string;
address: string;
account: number;
index: number;
change: boolean;
zone: Zone;
pubKey: string;
address: string;
account: number;
index: number;
change: boolean;
zone: Zone;
}

// Constant to represent the maximum attempt to derive an address
const MAX_ADDRESS_DERIVATION_ATTEMPTS = 10000000;

export abstract class AbstractHDWallet {
protected static _coinType?: AllowedCoinType;
protected static _coinType?: AllowedCoinType;

// Map of account number to HDNodeWallet
protected _accounts: Map<number, HDNodeWallet> = new Map();
Expand All @@ -32,7 +32,7 @@ export abstract class AbstractHDWallet {
// Root node of the HD wallet
protected _root: HDNodeWallet;

protected provider?: Provider;
protected provider?: Provider;

/**
* @private
Expand All @@ -43,44 +43,51 @@ export abstract class AbstractHDWallet {
}

protected static parentPath(coinType: number): string {
return `m/44'/${coinType}'`;
}
protected coinType(): number {
return (this.constructor as typeof AbstractHDWallet)._coinType!;
}
return `m/44'/${coinType}'`;
}

protected coinType(): number {
return (this.constructor as typeof AbstractHDWallet)._coinType!;
}

// helper methods that adds an account HD node to the HD wallet following the BIP-44 standard.
protected addAccount(accountIndex: number): void {
const newNode = this._root.deriveChild(accountIndex);
this._accounts.set(accountIndex, newNode);
}

protected deriveAddress(account: number, startingIndex: number, zone: Zone, isChange: boolean = false): HDNodeWallet {
protected deriveAddress(
account: number,
startingIndex: number,
zone: Zone,
isChange: boolean = false,
): HDNodeWallet {
this.validateZone(zone);
const isValidAddressForZone = (address: string) => {
const addressZone = getZoneForAddress(address);
if (!addressZone) {
return false;
}
const isCorrectShard = addressZone === zone;
const isCorrectLedger = (this.coinType() === 969) ? isQiAddress(address) : !isQiAddress(address);
return isCorrectShard && isCorrectLedger;
}
// derive the address node
const accountNode = this._accounts.get(account);
const changeIndex = isChange ? 1 : 0;
const changeNode = accountNode!.deriveChild(changeIndex);
let addrIndex: number = startingIndex;
let addressNode: HDNodeWallet;
do {
addressNode = changeNode.deriveChild(addrIndex);
addrIndex++;
// put a hard limit on the number of addresses to derive
if (addrIndex - startingIndex > MAX_ADDRESS_DERIVATION_ATTEMPTS) {
throw new Error(`Failed to derive a valid address for the zone ${zone} after ${MAX_ADDRESS_DERIVATION_ATTEMPTS} attempts.`);
}
} while (!isValidAddressForZone(addressNode.address));
const isCorrectShard = addressZone === zone;
const isCorrectLedger = this.coinType() === 969 ? isQiAddress(address) : !isQiAddress(address);
return isCorrectShard && isCorrectLedger;
};
// derive the address node
const accountNode = this._accounts.get(account);
const changeIndex = isChange ? 1 : 0;
const changeNode = accountNode!.deriveChild(changeIndex);
let addrIndex: number = startingIndex;
let addressNode: HDNodeWallet;
do {
addressNode = changeNode.deriveChild(addrIndex);
addrIndex++;
// put a hard limit on the number of addresses to derive
if (addrIndex - startingIndex > MAX_ADDRESS_DERIVATION_ATTEMPTS) {
throw new Error(
`Failed to derive a valid address for the zone ${zone} after ${MAX_ADDRESS_DERIVATION_ATTEMPTS} attempts.`,
);
}
} while (!isValidAddressForZone(addressNode.address));

return addressNode;
}
Expand Down Expand Up @@ -112,7 +119,7 @@ export abstract class AbstractHDWallet {

return neuteredAddressInfo;
}
public getNextAddress(accountIndex: number, zone: Zone): NeuteredAddressInfo {
public getNextAddress(accountIndex: number, zone: Zone): NeuteredAddressInfo {
this.validateZone(zone);
if (!this._accounts.has(accountIndex)) {
this.addAccount(accountIndex);
Expand Down Expand Up @@ -154,35 +161,55 @@ export abstract class AbstractHDWallet {
return Array.from(addresses).filter((addressInfo) => addressInfo.account === account);
}

public getAddressesForZone(zone: Zone): NeuteredAddressInfo[] {
public getAddressesForZone(zone: Zone): NeuteredAddressInfo[] {
this.validateZone(zone);
const addresses = this._addresses.values();
return Array.from(addresses).filter((addressInfo) => addressInfo.zone === zone);
}
const addresses = this._addresses.values();
return Array.from(addresses).filter((addressInfo) => addressInfo.zone === zone);
}

protected static createInstance<T extends AbstractHDWallet>(this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic): T {
protected static createInstance<T extends AbstractHDWallet>(
this: new (root: HDNodeWallet) => T,
mnemonic: Mnemonic,
): T {
const coinType = (this as any)._coinType;
const root = HDNodeWallet.fromMnemonic(mnemonic, (this as any).parentPath(coinType));
return new this(root);
}

static fromMnemonic<T extends AbstractHDWallet>(this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic): T {
return (this as any).createInstance(mnemonic);
}

static createRandom<T extends AbstractHDWallet>(this: new (root: HDNodeWallet) => T, password?: string, wordlist?: Wordlist): T {
if (password == null) { password = ""; }
if (wordlist == null) { wordlist = LangEn.wordlist(); }
const mnemonic = Mnemonic.fromEntropy(randomBytes(16), password, wordlist);
return (this as any).createInstance(mnemonic);
}

static fromPhrase<T extends AbstractHDWallet>(this: new (root: HDNodeWallet) => T, phrase: string, password?: string, wordlist?: Wordlist): T {
if (password == null) { password = ""; }
if (wordlist == null) { wordlist = LangEn.wordlist(); }
const mnemonic = Mnemonic.fromPhrase(phrase, password, wordlist);
return (this as any).createInstance(mnemonic);
}
const root = HDNodeWallet.fromMnemonic(mnemonic, (this as any).parentPath(coinType));
return new this(root);
}

static fromMnemonic<T extends AbstractHDWallet>(this: new (root: HDNodeWallet) => T, mnemonic: Mnemonic): T {
return (this as any).createInstance(mnemonic);
}

static createRandom<T extends AbstractHDWallet>(
this: new (root: HDNodeWallet) => T,
password?: string,
wordlist?: Wordlist,
): T {
if (password == null) {
password = '';
}
if (wordlist == null) {
wordlist = LangEn.wordlist();
}
const mnemonic = Mnemonic.fromEntropy(randomBytes(16), password, wordlist);
return (this as any).createInstance(mnemonic);
}

static fromPhrase<T extends AbstractHDWallet>(
this: new (root: HDNodeWallet) => T,
phrase: string,
password?: string,
wordlist?: Wordlist,
): T {
if (password == null) {
password = '';
}
if (wordlist == null) {
wordlist = LangEn.wordlist();
}
const mnemonic = Mnemonic.fromPhrase(phrase, password, wordlist);
return (this as any).createInstance(mnemonic);
}

abstract signTransaction(tx: TransactionRequest): Promise<string>;

Expand Down
Loading
Loading