Skip to content

Commit

Permalink
Merging the develop branch into the main branch, v5.2.0
Browse files Browse the repository at this point in the history
This merge contains the following set of changes:
  - Parsing extended relayer fee format (#149)
  • Loading branch information
akolotov authored Jul 14, 2023
2 parents 0d9ff54 + 9659494 commit 84471a7
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 42 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zkbob-client-js",
"version": "5.1.1",
"version": "5.2.0",
"description": "zkBob integration library",
"repository": "git@github.com:zkBob/libzkbob-client-js.git",
"author": "Dmitry Vdovin <voidxnull@gmail.com>",
Expand Down
41 changes: 38 additions & 3 deletions src/client-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,13 +383,48 @@ export class ZkBobProvider {
return cachedFee.fee;
}

protected async executionTxFee(txType: TxType, relayerFee?: RelayerFee): Promise<bigint> {
const fee = relayerFee ?? await this.getRelayerFee();
switch (txType) {
case TxType.Deposit: return fee.fee.deposit;
case TxType.Transfer: return fee.fee.transfer;
case TxType.Withdraw: return fee.fee.withdrawal;
case TxType.BridgeDeposit: return fee.fee.permittableDeposit;
default: throw new InternalError(`Unknown TxType: ${txType}`);
}
}

// Min transaction fee in pool resolution (for regular transaction without any payload overhead)
// To estimate fee for the concrete tx use account-based method (feeEstimate from client.ts)
public async atomicTxFee(txType: TxType): Promise<bigint> {
public async atomicTxFee(txType: TxType, withdrawSwap: bigint = 0n): Promise<bigint> {
const relayerFee = await this.getRelayerFee();
const calldataBytesCnt = estimateCalldataLength(txType, txType == TxType.Transfer ? 1 : 0);

return this.singleTxFeeInternal(relayerFee, txType, txType == TxType.Transfer ? 1 : 0, 0, withdrawSwap, true);
}

return this.roundFee(relayerFee.fee + relayerFee.oneByteFee * BigInt(calldataBytesCnt));
// dynamic fee calculation routine
protected async singleTxFeeInternal(
relayerFee: RelayerFee,
txType: TxType,
notesCnt: number,
extraDataLen: number = 0,
withdrawSwapAmount: bigint = 0n,
roundFee?: boolean,
): Promise<bigint> {
const calldataBytesCnt = estimateCalldataLength(txType, notesCnt, extraDataLen);
const baseFee = await this.executionTxFee(txType, relayerFee);

let totalFee = baseFee + relayerFee.oneByteFee * BigInt(calldataBytesCnt);
if (txType == TxType.Withdraw && withdrawSwapAmount > 0n) {
// swapping tokens during withdrawal may require additional fee
totalFee += relayerFee.nativeConvertFee;
}

if (roundFee === undefined || roundFee == true) {
totalFee = await this.roundFee(totalFee);
}

return totalFee;
}

// Max supported token swap during withdrawal, in token resolution (Gwei)
Expand Down
71 changes: 39 additions & 32 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ export class ZkBobClient extends ZkBobProvider {
// Fee estimating
const usedFee = relayerFee ?? await this.getRelayerFee();
const txType = pool.depositScheme == DepositType.Approve ? TxType.Deposit : TxType.BridgeDeposit;
let estimatedFee = await this.feeEstimateInternal([amountGwei], txType, usedFee, false, true);
let estimatedFee = await this.feeEstimateInternal([amountGwei], txType, usedFee, 0n, false, true);
const feeGwei = estimatedFee.total;

const deadline = Math.floor(Date.now() / 1000) + PERMIT_DEADLINE_INTERVAL;
Expand Down Expand Up @@ -698,7 +698,7 @@ export class ZkBobClient extends ZkBobProvider {
const pool = await this.pool();
const actualFee = relayerFee ?? await this.getRelayerFee();
const txType = pool.depositScheme == DepositType.Approve ? TxType.Deposit : TxType.BridgeDeposit;
const neededFee = await this.feeEstimateInternal([amountGwei], txType, actualFee, true, true);
const neededFee = await this.feeEstimateInternal([amountGwei], txType, actualFee, 0n, true, true);
if(fromAddress.tokenBalance < amountGwei + neededFee.total) {
throw new TxInsufficientFundsError(amountGwei + neededFee.total, fromAddress.tokenBalance);
}
Expand Down Expand Up @@ -783,10 +783,10 @@ export class ZkBobClient extends ZkBobProvider {
const txParts = await this.getTransactionParts(TxType.Transfer, transfers, usedFee);

if (txParts.length == 0) {
const available = await this.calcMaxAvailableTransfer(TxType.Transfer, false);
const available = await this.calcMaxAvailableTransfer(TxType.Transfer, usedFee, 0n, false);
const amounts = transfers.map((aTx) => aTx.amountGwei);
const totalAmount = amounts.reduce((acc, cur) => acc + cur, BigInt(0));
const feeEst = await this.feeEstimateInternal(amounts, TxType.Transfer, usedFee, false, true);
const feeEst = await this.feeEstimateInternal(amounts, TxType.Transfer, usedFee, 0n, false, true);
throw new TxInsufficientFundsError(totalAmount + feeEst.total, available);
}

Expand Down Expand Up @@ -886,8 +886,8 @@ export class ZkBobClient extends ZkBobProvider {
const txParts = await this.getTransactionParts(TxType.Withdraw, [{amountGwei, destination: address}], usedFee);

if (txParts.length == 0) {
const available = await this.calcMaxAvailableTransfer(TxType.Withdraw, false);
const feeEst = await this.feeEstimateInternal([amountGwei], TxType.Withdraw, usedFee, false, true);
const available = await this.calcMaxAvailableTransfer(TxType.Withdraw, usedFee, swapAmount, false);
const feeEst = await this.feeEstimateInternal([amountGwei], TxType.Withdraw, usedFee, swapAmount, false, true);
throw new TxInsufficientFundsError(amountGwei + feeEst.total, available);
}

Expand Down Expand Up @@ -1041,17 +1041,18 @@ export class ZkBobClient extends ZkBobProvider {
// There are two extra states in case of insufficient funds for requested token amount:
// 1. txCnt contains number of transactions for maximum available transfer
// 2. txCnt can't be less than 1 (e.g. when balance is less than atomic fee)
public async feeEstimate(transfersGwei: bigint[], txType: TxType, updateState: boolean = true): Promise<FeeAmount> {
public async feeEstimate(transfersGwei: bigint[], txType: TxType, withdrawSwap: bigint = 0n, updateState: boolean = true): Promise<FeeAmount> {
const relayerFee = await this.getRelayerFee();
return this.feeEstimateInternal(transfersGwei, txType, relayerFee, updateState, true);
return this.feeEstimateInternal(transfersGwei, txType, relayerFee, withdrawSwap, updateState, true);
}

private async feeEstimateInternal(
transfersGwei: bigint[],
txType: TxType,
relayerFee: RelayerFee,
withdrawSwap: bigint,
updateState: boolean,
roundFee: boolean
roundFee: boolean,
): Promise<FeeAmount> {
let txCnt = 1;
let total = 0n;
Expand All @@ -1061,7 +1062,7 @@ export class ZkBobClient extends ZkBobProvider {
if (txType === TxType.Transfer || txType === TxType.Withdraw) {
// we set allowPartial flag here to get parts anywhere
const requests: TransferRequest[] = transfersGwei.map((gwei) => { return {amountGwei: gwei, destination: NULL_ADDRESS} }); // destination address is ignored for estimation purposes
const parts = await this.getTransactionParts(txType, requests, relayerFee, updateState, true);
const parts = await this.getTransactionParts(txType, requests, relayerFee, withdrawSwap, updateState, true);
const totalBalance = await this.getTotalBalance(false);

const totalSumm = parts
Expand All @@ -1080,25 +1081,24 @@ export class ZkBobClient extends ZkBobProvider {
}
} else { // if we haven't funds for atomic fee - suppose we can make at least one tx
txCnt = 1;
total = await this.atomicTxFee(txType);
total = await this.atomicTxFee(txType, withdrawSwap);
calldataTotalLength = estimateCalldataLength(txType, txType == TxType.Transfer ? transfersGwei.length : 0);
}

insufficientFunds = (totalSumm < totalRequested || totalSumm + total > totalBalance) ? true : false;
} else {
// Deposit and BridgeDeposit cases are independent on the user balance
// Fee got from the native coins, so any deposit can be make within single tx
calldataTotalLength = estimateCalldataLength(txType, 0);
total = relayerFee.fee + relayerFee.oneByteFee * BigInt(calldataTotalLength);
total = roundFee ? await this.roundFee(total) : total;
calldataTotalLength = estimateCalldataLength(txType, 0)
total = await this.singleTxFeeInternal(relayerFee, txType, 0, 0, 0n, roundFee);
}

return {total, txCnt, calldataTotalLength, relayerFee, insufficientFunds};
}

// Account + notes balance excluding fee needed to transfer or withdraw it
// TODO: need to optimize for edge cases (account limit calculating)
public async calcMaxAvailableTransfer(txType: TxType, updateState: boolean = true): Promise<bigint> {
public async calcMaxAvailableTransfer(txType: TxType, relayerFee?: RelayerFee, withdrawSwap: bigint = 0n, updateState: boolean = true): Promise<bigint> {
if (txType != TxType.Transfer && txType != TxType.Withdraw) {
throw new InternalError(`Attempting to invoke \'calcMaxAvailableTransfer\' for ${txTypeToString(txType)} tx (only transfer\\withdraw are supported)`);
}
Expand All @@ -1108,11 +1108,9 @@ export class ZkBobClient extends ZkBobProvider {
await this.updateState();
}

const relayerFee = await this.getRelayerFee();
const aggregateTxLen = BigInt(estimateCalldataLength(TxType.Transfer, 0)); // aggregation txs are always transfers
const finalTxLen = BigInt(estimateCalldataLength(txType, txType == TxType.Transfer ? 1 : 0));
const aggregateTxFee = await this.roundFee(relayerFee.fee + aggregateTxLen * relayerFee.oneByteFee);
const finalTxFee = await this.roundFee(relayerFee.fee + finalTxLen * relayerFee.oneByteFee);
const usedFee = relayerFee ?? await this.getRelayerFee();
const aggregateTxFee = await this.singleTxFeeInternal(usedFee, TxType.Transfer, 0, 0, 0n);
const finalTxFee = await this.singleTxFeeInternal(usedFee, txType, txType == TxType.Transfer ? 1 : 0, 0, withdrawSwap);

const groupedNotesBalances = await this.getGroupedNotes();
let accountBalance = await state.accountBalance();
Expand Down Expand Up @@ -1142,7 +1140,8 @@ export class ZkBobClient extends ZkBobProvider {
public async getTransactionParts(
txType: TxType,
transfers: TransferRequest[],
relayerFee: RelayerFee,
relayerFee?: RelayerFee,
withdrawSwap: bigint = 0n,
updateState: boolean = true,
allowPartial: boolean = false,
): Promise<Array<TransferConfig>> {
Expand Down Expand Up @@ -1174,10 +1173,19 @@ export class ZkBobClient extends ZkBobProvider {

let aggregationParts: Array<TransferConfig> = [];
let txParts: Array<TransferConfig> = [];

const usedFee = relayerFee ?? await this.getRelayerFee();

let i = 0;
do {
txParts = await this.tryToPrepareTransfers(txType, accountBalance, relayerFee, groupedNotesBalances.slice(i, i + aggregatedTransfers.length), aggregatedTransfers);
txParts = await this.tryToPrepareTransfers(
txType,
accountBalance,
usedFee,
groupedNotesBalances.slice(i, i + aggregatedTransfers.length),
aggregatedTransfers,
withdrawSwap
);
if (txParts.length == aggregatedTransfers.length) {
// We are able to perform all txs starting from this index
return aggregationParts.concat(txParts);
Expand All @@ -1188,11 +1196,10 @@ export class ZkBobClient extends ZkBobProvider {
break;
}

const calldataLength = estimateCalldataLength(TxType.Transfer, 0);
const fee = await this.roundFee(relayerFee.fee + relayerFee.oneByteFee * BigInt(calldataLength));
const aggregateTxFee = await this.singleTxFeeInternal(usedFee, TxType.Transfer, 0, 0, 0n);

const inNotesBalance = groupedNotesBalances[i];
if (accountBalance + inNotesBalance < fee) {
if (accountBalance + inNotesBalance < aggregateTxFee) {
// We cannot collect amount to cover tx fee. There are 2 cases:
// insufficient balance or unoperable notes configuration
break;
Expand All @@ -1201,11 +1208,11 @@ export class ZkBobClient extends ZkBobProvider {
aggregationParts.push({
inNotesBalance,
outNotes: [],
calldataLength,
fee,
calldataLength: estimateCalldataLength(TxType.Transfer, 0),
fee: aggregateTxFee,
accountLimit: BigInt(0)
});
accountBalance += BigInt(inNotesBalance) - fee;
accountBalance += BigInt(inNotesBalance) - aggregateTxFee;

i++;
} while (i < groupedNotesBalances.length)
Expand All @@ -1220,16 +1227,16 @@ export class ZkBobClient extends ZkBobProvider {
balance: bigint,
relayerFee: RelayerFee,
groupedNotesBalances: Array<bigint>,
transfers: MultinoteTransferRequest[]
transfers: MultinoteTransferRequest[],
withdrawSwap: bigint = 0n,
): Promise<Array<TransferConfig>> {
let accountBalance = balance;
let parts: Array<TransferConfig> = [];
for (let i = 0; i < transfers.length; i++) {
const inNotesBalance = i < groupedNotesBalances.length ? groupedNotesBalances[i] : BigInt(0);

const numOfNotes = (txType == TxType.Transfer) ? transfers[i].requests.length : 0;
const calldataLength = estimateCalldataLength(txType, numOfNotes);
const fee = await this.roundFee(relayerFee.fee + relayerFee.oneByteFee * BigInt(calldataLength));
const fee = await this.singleTxFeeInternal(relayerFee, txType, numOfNotes, 0, withdrawSwap);

if (accountBalance + inNotesBalance < transfers[i].totalAmount + fee) {
// We haven't enough funds to perform such tx
Expand All @@ -1239,7 +1246,7 @@ export class ZkBobClient extends ZkBobProvider {
parts.push({
inNotesBalance,
outNotes: transfers[i].requests,
calldataLength,
calldataLength: estimateCalldataLength(txType, numOfNotes),
fee,
accountLimit: BigInt(0)
});
Expand Down
49 changes: 43 additions & 6 deletions src/services/relayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,14 @@ const isRelayerInfo = (obj: any): obj is RelayerInfo => {
}

export interface RelayerFee {
fee: bigint;
fee: {
deposit: bigint;
transfer: bigint;
withdrawal: bigint;
permittableDeposit: bigint;
};
oneByteFee: bigint;
nativeConvertFee: bigint;
}

interface Limit { // all values are in Gwei
Expand Down Expand Up @@ -228,8 +234,9 @@ export class ZkBobRelayer implements IZkBobService {
}

public async fee(): Promise<RelayerFee> {
const url = new URL('/fee', this.url());
const headers = defaultHeaders(this.supportId);
const url = new URL('/fee', this.url());

const res = await fetchJson(url.toString(), {headers}, this.type());

if (typeof res !== 'object' || res === null ||
Expand All @@ -238,10 +245,40 @@ export class ZkBobRelayer implements IZkBobService {
throw new ServiceError(this.type(), 200, 'Incorrect response for dynamic fees');
}

return {
fee: BigInt(res.fee ?? res.baseFee),
oneByteFee: BigInt(res.oneByteFee ?? '0')
};
const feeResp = res.fee ?? res.baseFee;
if (typeof feeResp === 'object' &&
feeResp.hasOwnProperty('deposit') &&
feeResp.hasOwnProperty('transfer') &&
feeResp.hasOwnProperty('withdrawal') &&
feeResp.hasOwnProperty('permittableDeposit')
){
return {
fee: {
deposit: BigInt(feeResp.deposit),
transfer: BigInt(feeResp.transfer),
withdrawal: BigInt(feeResp.withdrawal),
permittableDeposit: BigInt(feeResp.permittableDeposit),
},
oneByteFee: BigInt(res.oneByteFee ?? '0'),
nativeConvertFee: BigInt(res.nativeConvertFee ?? '0')
};
} else if (typeof feeResp === 'string' ||
typeof feeResp === 'number' ||
typeof feeResp === 'bigint'
) {
return {
fee: {
deposit: BigInt(feeResp),
transfer: BigInt(feeResp),
withdrawal: BigInt(feeResp),
permittableDeposit: BigInt(feeResp),
},
oneByteFee: BigInt(res.oneByteFee ?? '0'),
nativeConvertFee: BigInt(res.nativeConvertFee ?? '0')
};
} else {
throw new ServiceError(this.type(), 200, 'Incorrect fee field');
}
}

public async limits(address: string | undefined): Promise<LimitsFetch> {
Expand Down

0 comments on commit 84471a7

Please sign in to comment.