Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* refactor coin selection logic to work with denomination indices

* refactor coinselector to work with UTXO objects

* add new method signature for sendTransaction using paymentCodes

* implement sendTransaction with paymentCode

* update proto schema for Qi

* WIP: add lock field and debug lines

* Fix Qi tx submission

* fix send Qi with musig

* Remove redundent tx type population

* Add qi tx fee and force denominating down for outputs

* Fix signature decision in qi tx signing

* Fix import without file ext

* Export serialized wallet types

* Remove console logs

* Update external deps reference

* Apply automatic changes

* Apply automatic changes

* Fix vulnerable dependency `rollup`

* Apply automatic changes

---------

Co-authored-by: Alejo Acosta <[email protected]>
Co-authored-by: rileystephens28 <[email protected]>
  • Loading branch information
3 people committed Oct 8, 2024
1 parent d4cafe1 commit 0dab9d7
Show file tree
Hide file tree
Showing 12 changed files with 717 additions and 326 deletions.
80 changes: 80 additions & 0 deletions examples/wallets/qi-send.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const quais = require('../../lib/commonjs/quais');
require('dotenv').config();

// Descrepancy between our serialized data and go quais in that ours in inlcude extra data at the end -> 201406c186bf3b66571cfdd8c7d9336df2298e4d4a9a2af7fcca36fbdfb0b43459a41c45b6c8885dc1f828d44fd005572cbac4cd72dc598790429255d19ec32f7750e

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);
aliceQiWallet.connect(provider);

// Initialize Qi wallet
console.log('Initializing Alice wallet...');
await aliceQiWallet.scan(quais.Zone.Cyprus1);
console.log('Alice wallet scan complete');
console.log('Serializing Alice wallet...');
const serializedWallet = aliceQiWallet.serialize();

const summary = {
'Total Addresses': serializedWallet.addresses.length,
'Change Addresses': serializedWallet.changeAddresses.length,
'Gap Addresses': serializedWallet.gapAddresses.length,
'Gap Change Addresses': serializedWallet.gapChangeAddresses.length,
Outpoints: serializedWallet.outpoints.length,
'Coin Type': serializedWallet.coinType,
Version: serializedWallet.version,
};

console.log('Alice Wallet Summary:');
console.table(summary);

const addressTable = serializedWallet.addresses.map((addr) => ({
PubKey: addr.pubKey,
Address: addr.address,
Index: addr.index,
Change: addr.change ? 'Yes' : 'No',
Zone: addr.zone,
}));

console.log('\nAlice Wallet Addresses (first 10):');
console.table(addressTable.slice(0, 10));

const outpointsInfoTable = serializedWallet.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.log('\nAlice Outpoints Info (first 10):');
console.table(outpointsInfoTable.slice(0, 10));

console.log(`Generating Bob's wallet and payment code...`);
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);
const bobPaymentCode = await bobQiWallet.getPaymentCode(0);
console.log('Bob Payment code: ', bobPaymentCode);

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

// Alice sends 1000 Qi to Bob
const tx = await aliceQiWallet.sendTransaction(bobPaymentCode, 750000, quais.Zone.Cyprus1, quais.Zone.Cyprus1);
console.log('Transaction sent: ', tx);
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
267 changes: 242 additions & 25 deletions src/_tests/unit/coinselection.unit.test.ts

Large diffs are not rendered by default.

19 changes: 16 additions & 3 deletions src/providers/provider-jsonrpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ export abstract class JsonRpcApiProvider<C = FetchRequest> extends AbstractProvi
if (tx && tx.type != null && getBigInt(tx.type)) {
// If there are no EIP-1559 properties, it might be non-EIP-a559
if (tx.maxFeePerGas == null && tx.maxPriorityFeePerGas == null) {
const feeData = await this.getFeeData(req.zone);
const feeData = await this.getFeeData(req.zone, tx.type === 1); // tx type 1 is Quai and 2 is Qi
if (feeData.maxFeePerGas == null && feeData.maxPriorityFeePerGas == null) {
// Network doesn't know about EIP-1559 (and hence type)
req = Object.assign({}, req, {
Expand Down Expand Up @@ -1119,7 +1119,6 @@ export abstract class JsonRpcApiProvider<C = FetchRequest> extends AbstractProvi
(<any>result)[dstKey] = toQuantity(getBigInt((<any>tx)[key], `tx.${key}`));
});

// Make sure addresses and data are lowercase
['from', 'to', 'data'].forEach((key) => {
if ((<any>tx)[key] == null) {
return;
Expand All @@ -1132,8 +1131,22 @@ export abstract class JsonRpcApiProvider<C = FetchRequest> extends AbstractProvi
(result as QuaiJsonRpcTransactionRequest)['accessList'] = accessListify(tx.accessList);
}
} else {
throw new Error('No Qi getRPCTransaction implementation yet');
if ((<any>tx).txInputs != null) {
(result as QiJsonRpcTransactionRequest)['txInputs'] = (<any>tx).txInputs.map((input: TxInput) => ({
txhash: hexlify(input.txhash),
index: toQuantity(getBigInt(input.index, `tx.txInputs.${input.index}`)),
pubkey: hexlify(input.pubkey),
}));
}

if ((<any>tx).txOutputs != null) {
(result as QiJsonRpcTransactionRequest)['txOutputs'] = (<any>tx).txOutputs.map((output: TxOutput) => ({
address: hexlify(output.address),
denomination: toQuantity(getBigInt(output.denomination, `tx.txOutputs.${output.denomination}`)),
}));
}
}

return result;
}

Expand Down
3 changes: 2 additions & 1 deletion src/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2743,9 +2743,10 @@ export interface Provider extends ContractRunner, EventEmitterable<ProviderEvent
* Get the best guess at the recommended {@link FeeData | **FeeData**}.
*
* @param {Zone} zone - The shard to fetch the fee data from.
* @param {boolean} txType - The transaction type to fetch the fee data for (true for Quai, false for Qi)
* @returns {Promise<FeeData>} A promise resolving to the fee data.
*/
getFeeData(zone: Zone): Promise<FeeData>;
getFeeData(zone: Zone, txType: boolean): Promise<FeeData>;

/**
* Get a work object to package a transaction in.
Expand Down
2 changes: 2 additions & 0 deletions src/quais.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ export {
decryptKeystoreJson,
encryptKeystoreJson,
encryptKeystoreJsonSync,
SerializedHDWallet,
SerializedQiHDWallet,
} from './wallet/index.js';

// WORDLIST
Expand Down
6 changes: 1 addition & 5 deletions src/signers/abstract-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,6 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
pop.nonce = await this.getNonce('pending');
}

if (pop.type == null) {
pop.type = getTxType(pop.from ?? null, pop.to ?? null);
}

if (pop.gasLimit == null) {
if (pop.type == 0) pop.gasLimit = await this.estimateGas(pop);
else {
Expand All @@ -138,7 +134,7 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
pop.chainId = network.chainId;
}
if (pop.maxFeePerGas == null || pop.maxPriorityFeePerGas == null) {
const feeData = await provider.getFeeData(zone);
const feeData = await provider.getFeeData(zone, true);

if (pop.maxFeePerGas == null) {
pop.maxFeePerGas = feeData.maxFeePerGas;
Expand Down
79 changes: 12 additions & 67 deletions src/transaction/abstract-coinselector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UTXO, UTXOLike } from './utxo.js';
import { UTXO } from './utxo.js';

/**
* Represents a target for spending.
Expand Down Expand Up @@ -38,80 +38,25 @@ export type SelectedCoinsResult = {
* @abstract
*/
export abstract class AbstractCoinSelector {
#availableUTXOs: UTXO[];
#spendOutputs: UTXO[];
#changeOutputs: UTXO[];

/**
* Gets the available UTXOs.
*
* @returns {UTXO[]} The available UTXOs.
*/
get availableUTXOs(): UTXO[] {
return this.#availableUTXOs;
}

/**
* Sets the available UTXOs.
*
* @param {UTXOLike[]} value - The UTXOs to set.
*/
set availableUTXOs(value: UTXOLike[]) {
this.#availableUTXOs = value.map((val) => {
const utxo = UTXO.from(val);
this._validateUTXO(utxo);
return utxo;
});
}

/**
* Gets the spend outputs.
*
* @returns {UTXO[]} The spend outputs.
*/
get spendOutputs(): UTXO[] {
return this.#spendOutputs;
}

/**
* Sets the spend outputs.
*
* @param {UTXOLike[]} value - The spend outputs to set.
*/
set spendOutputs(value: UTXOLike[]) {
this.#spendOutputs = value.map((utxo) => UTXO.from(utxo));
}

/**
* Gets the change outputs.
*
* @returns {UTXO[]} The change outputs.
*/
get changeOutputs(): UTXO[] {
return this.#changeOutputs;
}

/**
* Sets the change outputs.
*
* @param {UTXOLike[]} value - The change outputs to set.
*/
set changeOutputs(value: UTXOLike[]) {
this.#changeOutputs = value.map((utxo) => UTXO.from(utxo));
}
public availableUTXOs: UTXO[];
public totalInputValue: bigint = BigInt(0);
public spendOutputs: UTXO[] = [];
public changeOutputs: UTXO[] = [];
public selectedUTXOs: UTXO[] = [];
public target: bigint | null = null;

/**
* Constructs a new AbstractCoinSelector instance with an empty UTXO array.
*
* @param {UTXOEntry[]} [availableUXTOs=[]] - The initial available UTXOs. Default is `[]`
* @param {UTXO[]} [availableUXTOs=[]] - The initial available UTXOs. Default is `[]`
*/
constructor(availableUTXOs: UTXO[] = []) {
this.#availableUTXOs = availableUTXOs.map((utxo: UTXO) => {
this.availableUTXOs = availableUTXOs.map((utxo: UTXO) => {
this._validateUTXO(utxo);
return utxo;
});
this.#spendOutputs = [];
this.#changeOutputs = [];
this.spendOutputs = [];
this.changeOutputs = [];
}

/**
Expand All @@ -123,7 +68,7 @@ export abstract class AbstractCoinSelector {
* @param {SpendTarget} target - The target address and value to spend.
* @returns {SelectedCoinsResult} The selected UTXOs and outputs.
*/
abstract performSelection(target: SpendTarget): SelectedCoinsResult;
abstract performSelection(target: bigint): SelectedCoinsResult;

/**
* Validates the provided UTXO instance. In order to be valid for coin selection, the UTXO must have a valid address
Expand Down
Loading

0 comments on commit 0dab9d7

Please sign in to comment.