Skip to content

Commit

Permalink
Qi gas fee (#309)
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 10, 2024
1 parent be9c31f commit b852a26
Show file tree
Hide file tree
Showing 19 changed files with 864 additions and 322 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.

1 change: 1 addition & 0 deletions src/encoding/protoc/proto_block.proto
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,5 @@ message ProtoOutPoint {
message ProtoTxOut {
optional uint32 denomination = 1;
optional bytes address = 2;
optional bytes lock = 3;
}
40 changes: 38 additions & 2 deletions src/encoding/protoc/proto_block.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Generated by the protoc-gen-ts. DO NOT EDIT!
* compiler version: 4.25.3
* compiler version: 4.24.3
* source: proto_block.proto
* git: https://github.com/thesayyn/protoc-gen-ts */
import * as dependency_1 from "./proto_common.js";
Expand Down Expand Up @@ -4394,11 +4394,13 @@ export namespace block {
}
}
export class ProtoTxOut extends pb_1.Message {
#one_of_decls: number[][] = [[1], [2]];
#one_of_decls: number[][] = [[1], [2], [3]];
constructor(data?: any[] | ({} & (({
denomination?: number;
}) | ({
address?: Uint8Array;
}) | ({
lock?: Uint8Array;
})))) {
super();
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
Expand All @@ -4409,6 +4411,9 @@ export namespace block {
if ("address" in data && data.address != undefined) {
this.address = data.address;
}
if ("lock" in data && data.lock != undefined) {
this.lock = data.lock;
}
}
}
get denomination() {
Expand All @@ -4429,6 +4434,15 @@ export namespace block {
get has_address() {
return pb_1.Message.getField(this, 2) != null;
}
get lock() {
return pb_1.Message.getFieldWithDefault(this, 3, new Uint8Array(0)) as Uint8Array;
}
set lock(value: Uint8Array) {
pb_1.Message.setOneofField(this, 3, this.#one_of_decls[2], value);
}
get has_lock() {
return pb_1.Message.getField(this, 3) != null;
}
get _denomination() {
const cases: {
[index: number]: "none" | "denomination";
Expand All @@ -4447,9 +4461,19 @@ export namespace block {
};
return cases[pb_1.Message.computeOneofCase(this, [2])];
}
get _lock() {
const cases: {
[index: number]: "none" | "lock";
} = {
0: "none",
3: "lock"
};
return cases[pb_1.Message.computeOneofCase(this, [3])];
}
static fromObject(data: {
denomination?: number;
address?: Uint8Array;
lock?: Uint8Array;
}): ProtoTxOut {
const message = new ProtoTxOut({});
if (data.denomination != null) {
Expand All @@ -4458,19 +4482,26 @@ export namespace block {
if (data.address != null) {
message.address = data.address;
}
if (data.lock != null) {
message.lock = data.lock;
}
return message;
}
toObject() {
const data: {
denomination?: number;
address?: Uint8Array;
lock?: Uint8Array;
} = {};
if (this.denomination != null) {
data.denomination = this.denomination;
}
if (this.address != null) {
data.address = this.address;
}
if (this.lock != null) {
data.lock = this.lock;
}
return data;
}
serialize(): Uint8Array;
Expand All @@ -4481,6 +4512,8 @@ export namespace block {
writer.writeUint32(1, this.denomination);
if (this.has_address)
writer.writeBytes(2, this.address);
if (this.has_lock)
writer.writeBytes(3, this.lock);
if (!w)
return writer.getResultBuffer();
}
Expand All @@ -4496,6 +4529,9 @@ export namespace block {
case 2:
message.address = reader.readBytes();
break;
case 3:
message.lock = reader.readBytes();
break;
default: reader.skipField();
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/encoding/protoc/proto_common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Generated by the protoc-gen-ts. DO NOT EDIT!
* compiler version: 4.25.3
* compiler version: 4.24.3
* source: proto_common.proto
* git: https://github.com/thesayyn/protoc-gen-ts */
import * as pb_1 from "google-protobuf";
Expand Down
63 changes: 37 additions & 26 deletions src/providers/abstract-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,7 @@ import type { BigNumberish } from '../utils/index.js';
import type { Listener } from '../utils/index.js';

import type { Networkish } from './network.js';
import type {
BlockParams,
LogParams,
OutpointResponseParams,
QiTransactionResponseParams,
TransactionReceiptParams,
TransactionResponseParams,
} from './formatting.js';
import type { BlockParams, LogParams, OutpointResponseParams, TransactionReceiptParams } from './formatting.js';

import type {
BlockTag,
Expand Down Expand Up @@ -1020,11 +1013,21 @@ export class AbstractProvider<C = FetchRequest> implements Provider {
*/
// TODO: `newtork` is not used, remove or re-write
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_wrapTransactionResponse(tx: TransactionResponseParams, network: Network): TransactionResponse {
if ('from' in tx) {
return new QuaiTransactionResponse(formatTransactionResponse(tx) as QuaiTransactionResponseParams, this);
} else {
return new QiTransactionResponse(formatTransactionResponse(tx) as QiTransactionResponseParams, this);
_wrapTransactionResponse(tx: any, network: Network): TransactionResponse {
try {
if (tx.type === 0 || tx.type === 1) {
// For QuaiTransaction, format and wrap as before
const formattedTx = formatTransactionResponse(tx) as QuaiTransactionResponseParams;
return new QuaiTransactionResponse(formattedTx, this);
} else if (tx.type === 2) {
// For QiTransaction, use fromProto() directly
return new QiTransactionResponse(tx, this);
} else {
throw new Error('Unknown transaction type');
}
} catch (error) {
console.error('Error in _wrapTransactionResponse:', error);
throw error;
}
}

Expand Down Expand Up @@ -1529,25 +1532,33 @@ export class AbstractProvider<C = FetchRequest> implements Provider {
// Write
async broadcastTransaction(zone: Zone, signedTx: string): Promise<TransactionResponse> {
const type = decodeProtoTransaction(getBytes(signedTx)).type;
const { blockNumber, hash, network } = await resolveProperties({
blockNumber: this.getBlockNumber(toShard(zone)),
hash: this._perform({
method: 'broadcastTransaction',
signedTransaction: signedTx,
zone: zone,
}),
network: this.getNetwork(),
});
try {
const { blockNumber, hash, network } = await resolveProperties({
blockNumber: this.getBlockNumber(toShard(zone)),
hash: this._perform({
method: 'broadcastTransaction',
signedTransaction: signedTx,
zone: zone,
}),
network: this.getNetwork(),
});

const tx = type == 2 ? QiTransaction.from(signedTx) : QuaiTransaction.from(signedTx);
const tx = type == 2 ? QiTransaction.from(signedTx) : QuaiTransaction.from(signedTx);
const txObj = tx.toJSON();

this.#validateTransactionHash(tx.hash || '', hash);
return this._wrapTransactionResponse(<any>tx, network).replaceableTransaction(blockNumber);
this.#validateTransactionHash(tx.hash || '', hash);

const wrappedTx = this._wrapTransactionResponse(<any>txObj, network);
return wrappedTx.replaceableTransaction(blockNumber);
} catch (error) {
console.error('Error in broadcastTransaction:', error);
throw error;
}
}

#validateTransactionHash(computedHash: string, nodehash: string) {
if (computedHash !== nodehash) {
throw new Error('Transaction hash mismatch');
throw new Error(`Transaction hash mismatch: ${computedHash} !== ${nodehash}`);
}
}

Expand Down
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
Loading

0 comments on commit b852a26

Please sign in to comment.