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

fix: fix dust error in Nexa(OK-27719) #4562

Merged
merged 11 commits into from
May 10, 2024
23 changes: 23 additions & 0 deletions packages/engine/src/vaults/VaultBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,29 @@ export abstract class VaultBase extends VaultBaseChainOnly {
return nextNonce;
}

getConfirmedUTXOs<T extends { value: string | number }>(
utxos: T[],
amount: string,
minTransferAmount = '0',
): T[] {
const transactionAmount = new BigNumber(amount).plus(minTransferAmount);
const confirmedUTXOs = utxos.sort((a, b) =>
new BigNumber(b.value).gt(a.value) ? 1 : -1,
);
let sum = new BigNumber(0);
let i = 0;
for (i = 0; i < confirmedUTXOs.length; i += 1) {
sum = sum.plus(confirmedUTXOs[i].value);
if (sum.gt(transactionAmount)) {
break;
}
}
if (sum.lt(transactionAmount)) {
return [];
}
return confirmedUTXOs.slice(0, i + 1);
}

huhuanming marked this conversation as resolved.
Show resolved Hide resolved
validateSendAmount(amount: string, tokenBalance: string, to: string) {
return Promise.resolve(true);
}
Expand Down
74 changes: 62 additions & 12 deletions packages/engine/src/vaults/impl/nexa/Vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,14 @@ import type {
} from '../../../types/provider';
import type { Token } from '../../../types/token';
import type { KeyringSoftwareBase } from '../../keyring/KeyringSoftwareBase';
import type { IDecodedTxLegacy, IHistoryTx, ISignedTxPro } from '../../types';
import type {
IDecodedTxLegacy,
IHistoryTx,
ISignCredentialOptions,
ISignedTxPro,
} from '../../types';
import type { EVMDecodedItem } from '../evm/decoder/types';
import type { IEncodedTxNexa, INexaTransaction } from './types';
import type { IEncodedTxNexa, IListUTXO, INexaTransaction } from './types';

export default class Vault extends VaultBase {
keyringMap = {
Expand Down Expand Up @@ -243,13 +248,12 @@ export default class Vault extends VaultBase {
): Promise<IEncodedTxNexa> {
const client = await this.getSDKClient();
const fromNexaAddress = transferInfo.from;
const utxos = (await client.getNexaUTXOs(fromNexaAddress)).filter(
const uxtos = (await client.getNexaUTXOs(fromNexaAddress)).filter(
(value) => !value.has_token,
);

const network = await this.getNetwork();
return {
inputs: utxos.map((utxo) => ({
inputs: uxtos.map((utxo) => ({
txId: utxo.outpoint_hash,
outputIndex: utxo.tx_pos,
satoshis: new BigNumber(utxo.value).toFixed(),
Expand Down Expand Up @@ -284,15 +288,51 @@ export default class Vault extends VaultBase {
return Promise.resolve(encodedTx);
}

override buildUnsignedTxFromEncodedTx(
async getMinTransferAmount() {
const network = await this.getNetwork();
return network.settings.minTransferAmount
? new BigNumber(network.settings.minTransferAmount)
.shiftedBy(network.decimals)
.toFixed()
: undefined;
}

override async buildUnsignedTxFromEncodedTx(
encodedTx: IEncodedTxNexa,
): Promise<IUnsignedTxPro> {
return Promise.resolve({
const client = await this.getSDKClient();
const network = await this.getNetwork();
const confirmedUTXOs = this.getConfirmedUTXOs(
encodedTx.inputs.map((input) => ({
...input,
value: input.satoshis,
})),
new BigNumber(encodedTx.transferInfo?.amount || 0)
.shiftedBy(network.decimals)
.plus(encodedTx?.gas || 0)
.toFixed(),
await this.getMinTransferAmount(),
);

if (confirmedUTXOs.length > client.MAX_TX_NUM_VIN) {
const maximumAmount = confirmedUTXOs
.slice(0, client.MAX_TX_NUM_VIN)
.reduce((acc, cur) => acc.plus(cur.value), new BigNumber(0));
throw new OneKeyInternalError(
`Too many vins, The maximum amount for this transfer is ${maximumAmount
.shiftedBy(-network.decimals)
.toFixed()} ${network.symbol}.`,
);
}
encodedTx.inputs = confirmedUTXOs;
return {
inputs: [],
outputs: [],
payload: { encodedTx },
payload: {
encodedTx,
},
encodedTx,
});
};
}

override async getTransactionStatuses(
Expand All @@ -309,11 +349,21 @@ export default class Vault extends VaultBase {
): Promise<IFeeInfo> {
const network = await this.getNetwork();
const client = await this.getSDKClient();
const estimateSizedSize = estimateSize(encodedTx);
const vinLength = this.getConfirmedUTXOs(
encodedTx.inputs.map((input) => ({
...input,
value: input.satoshis,
})),
new BigNumber(encodedTx.transferInfo?.amount || 0)
.shiftedBy(network.decimals)
.toFixed(),
await this.getMinTransferAmount(),
).length;
const estimateSizedSize = estimateSize(vinLength, encodedTx.outputs);
const remoteEstimateFee = await client.estimateFee(estimateSizedSize);
const localEstimateFee = estimateFee(encodedTx);
const localEstimateFee = estimateFee(vinLength, encodedTx.outputs);
const feeInfo = specifiedFeeRate
? estimateFee(encodedTx, Number(specifiedFeeRate))
? estimateFee(vinLength, encodedTx.outputs, Number(specifiedFeeRate))
: Math.max(remoteEstimateFee, localEstimateFee);
return {
nativeSymbol: network.symbol,
Expand Down
3 changes: 2 additions & 1 deletion packages/engine/src/vaults/impl/nexa/sdk/nexa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type { IListUTXO, INexaHistoryItem, INexaTransaction } from '../types';
export class Nexa extends SimpleClient {
readonly rpc: WebSocketRequest;

readonly MAX_TX_NUM_VIN = 256;

constructor(
url: string,
readonly defaultFinality: 'optimistic' | 'final' = 'optimistic',
Expand Down Expand Up @@ -58,7 +60,6 @@ export class Nexa extends SimpleClient {
async getTransaction(txHash: string): Promise<INexaTransaction> {
return this.rpc.call<INexaTransaction>('blockchain.transaction.get', [
txHash,
true,
]);
}

Expand Down
16 changes: 10 additions & 6 deletions packages/engine/src/vaults/impl/nexa/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,25 +142,29 @@ const DEFAULT_SEQNUMBER = MAXINT;
const FEE_PER_KB = 1000 * 3;
const CHANGE_OUTPUT_MAX_SIZE = 1 + 8 + 1 + 23;

export function estimateSize(encodedTx: IEncodedTxNexa) {
export function estimateSize(
vinlength: number,
vouts: IEncodedTxNexa['outputs'],
) {
let estimatedSize = 4 + 1; // locktime + version
estimatedSize += encodedTx.inputs.length < 253 ? 1 : 3;
encodedTx.inputs.forEach(() => {
estimatedSize += vinlength < 253 ? 1 : 3;
new Array(vinlength).fill(0).forEach(() => {
// type + outpoint + scriptlen + script + sequence + amount
estimatedSize += 1 + 32 + 1 + 100 + 4 + 8;
});
encodedTx.outputs.forEach((output) => {
vouts.forEach((output) => {
const bfr = getScriptBufferFromScriptTemplateOut(output.address);
estimatedSize += convertScriptToPushBuffer(bfr).length + 1 + 8 + 1;
});
return estimatedSize;
}

export function estimateFee(
encodedTx: IEncodedTxNexa,
vinlength: number,
vouts: IEncodedTxNexa['outputs'],
feeRate = FEE_PER_KB / 1000,
): number {
const size = estimateSize(encodedTx);
const size = estimateSize(vinlength, vouts);
const feeWithChange = Math.ceil(
size * feeRate + CHANGE_OUTPUT_MAX_SIZE * feeRate,
);
Expand Down