Skip to content

Commit

Permalink
Merge pull request #96 from capt-nemo429/improve-change-detection
Browse files Browse the repository at this point in the history
Refactor and improve change detection
  • Loading branch information
capt-nemo429 committed May 27, 2023
2 parents 9277037 + 646673a commit 6e7c974
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 53 deletions.
97 changes: 56 additions & 41 deletions src/api/ergo/transaction/interpreter/txInterpreter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { ERG_TOKEN_ID, MAINNET_MINER_FEE_TREE } from "@/constants/ergo";
import { ErgoBoxCandidate, Token, UnsignedTx } from "@/types/connector";
import { ErgoBoxCandidate, Token, UnsignedInput, UnsignedTx } from "@/types/connector";
import { StateAssetInfo } from "@/types/internal";
import { decimalize, sumBigNumberBy, toBigNumber } from "@/utils/bigNumbers";
import BigNumber from "bignumber.js";
import { difference, find, groupBy, isEmpty } from "lodash";
import { addressFromErgoTree } from "../../addresses";
import { isBabelContract } from "../../babelFees";
import { OutputAsset, OutputInterpreter } from "./outputInterpreter";
import { utxoSum, utxoSumResultDiff } from "@fleet-sdk/common";
import { some, utxoSum, utxoSumResultDiff } from "@fleet-sdk/common";
import {
tokensToOutputAssets,
boxCandidateToBoxAmounts,
Expand All @@ -28,7 +28,7 @@ export class TxInterpreter {
private _assetInfo!: StateAssetInfo;
private _addresses!: string[];
private _burningBalance!: OutputAsset[];
private _ownInputs!: ErgoBoxCandidate[];
private _ownInputs!: UnsignedInput[];
private _ownOutputs!: ErgoBoxCandidate[];
private _totalIncoming!: OutputAsset[];
private _totalLeaving!: OutputAsset[];
Expand All @@ -40,25 +40,26 @@ export class TxInterpreter {
this._feeBox = find(tx.outputs, (b) => isMinerFeeContract(b.ergoTree));

const isOwnErgoTree = (tree: string) => this._addresses.includes(addressFromErgoTree(tree));
const isSendingOutput = (output: ErgoBoxCandidate) =>
output !== this._feeBox && !this._changeBoxes.includes(output);

this._ownInputs = tx.inputs.filter((b) => isOwnErgoTree(b.ergoTree));
this._ownOutputs = tx.outputs.filter((b) => isOwnErgoTree(b.ergoTree));

this._calcIncomingLeavingTotals();

this._changeBoxes = this._determineChangeBoxes();
this._sendingBoxes = tx.outputs.filter(isSendingOutput);
this._calcIncomingLeavingTotals();

this._sendingBoxes = difference(tx.outputs, [this._feeBox, ...this._changeBoxes]).filter(
(b) => b !== undefined
) as ErgoBoxCandidate[];

if (this._changeBoxes.length > 0 && this._sendingBoxes.length <= 1) {
if (some(this._changeBoxes)) {
if (isEmpty(this._sendingBoxes)) {
this._sendingBoxes.push(this._changeBoxes.pop() as ErgoBoxCandidate);
this._sendingBoxes = this._changeBoxes;
this._changeBoxes = [];
} else if (
this._sendingBoxes.length === 1 &&
isBabelContract(this._sendingBoxes[0].ergoTree) &&
!isEmpty(this._sendingBoxes[0].assets)
) {
this._sendingBoxes.unshift(this._changeBoxes.pop() as ErgoBoxCandidate);
this._sendingBoxes = [...this._changeBoxes, ...this._sendingBoxes];
this._changeBoxes = [];
}
}

Expand Down Expand Up @@ -222,45 +223,59 @@ export class TxInterpreter {
}

private _determineChangeBoxes(): ErgoBoxCandidate[] {
const changeBoxes: ErgoBoxCandidate[] = [];
if (isEmpty(this._ownInputs) || isEmpty(this._ownOutputs)) {
return [];
}

const inputAssets = utxoSum(this._ownInputs);
const outputAssets = utxoSum(this._ownOutputs);
const diff = utxoSumResultDiff(inputAssets, outputAssets);

// handle intrawallet transactions, in this case we consider change as
// the own boxes after the fee box
if (diff.nanoErgs <= BigInt(this._feeBox?.value || 0)) {
const index = this._tx.outputs.findIndex((output) => isMinerFeeContract(output.ergoTree));

if (index > -1 && index < this._tx.outputs.length) {
const possiblyChange = this._tx.outputs.slice(index + 1);
return this._ownOutputs.filter((output) => possiblyChange.includes(output));
}

if (this._ownInputs.length === 0 || this._ownOutputs.length === 0) {
return [];
}

// Start with a map containing all own input assets and greedily
// subtract the amounts from own outputs when possible.
// This will detect some subset of outputs that form a valid
// set of own change boxes that is maximal in terms of inclusion.
const remainingOwnInputAssets = this._sumTokens(
this._ownInputs
.filter((x) => x.assets)
.map((x) => x.assets)
.flat()
const remainingInputTokens = Object.fromEntries(
inputAssets.tokens.map(({ tokenId, amount }) => [tokenId, amount])
);
if (!remainingOwnInputAssets) {
return [];
}
// Consider outputs with most assets first in an attempt to include as many tokens as possible
const sortedOwnOutputs = [...this._ownOutputs].sort((a, b) => {
return b.assets.length - a.assets.length;
});
for (const o of sortedOwnOutputs) {
const nonChangeAssets = o.assets.filter((asset) => {
return (
remainingOwnInputAssets[asset.tokenId] === undefined ||
remainingOwnInputAssets[asset.tokenId].isLessThan(asset.amount)
);
});
if (nonChangeAssets.length === 0) {
o.assets.forEach((asset) => {
remainingOwnInputAssets[asset.tokenId] = remainingOwnInputAssets[asset.tokenId].minus(
asset.amount
);
});
changeBoxes.push(o);
let remainingInputErg = inputAssets.nanoErgs;

const changeBoxes: ErgoBoxCandidate[] = [];
for (const output of this._ownOutputs) {
const outputValue = BigInt(output.value);
if (remainingInputErg < outputValue) {
continue;
}

const containsNonChangeAssets = !!output.assets.find(
(token) =>
!remainingInputTokens[token.tokenId] ||
remainingInputTokens[token.tokenId] < BigInt(token.amount)
);

if (!containsNonChangeAssets) {
remainingInputErg -= outputValue;
for (const asset of output.assets) {
remainingInputTokens[asset.tokenId] -= BigInt(asset.amount);
}

changeBoxes.push(output);
}
}

return changeBoxes;
}
}
6 changes: 3 additions & 3 deletions src/api/ergo/transaction/interpreter/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { OutputAsset } from "@/api/ergo/transaction/interpreter/outputInterpreter";
import { decimalize, toBigNumber } from "@/utils/bigNumbers";
import { StateAssetInfo } from "@/types/internal";
import { ErgoBoxCandidate, Token } from "@/types/connector";
import { ErgoBoxCandidate, Token, UnsignedInput } from "@/types/connector";
import { Amount, TokenAmount } from "@fleet-sdk/common";

export const tokensToOutputAssets = (tokens: Token[], assetInfo: StateAssetInfo): OutputAsset[] => {
Expand All @@ -16,9 +16,9 @@ export const tokensToOutputAssets = (tokens: Token[], assetInfo: StateAssetInfo)
});
};

export const boxCandidateToBoxAmounts = (b: ErgoBoxCandidate) => {
export const boxCandidateToBoxAmounts = (b: ErgoBoxCandidate | UnsignedInput) => {
return {
value: String(b.value),
value: b.value.toString(),
assets: b.assets
};
};
Expand Down
9 changes: 2 additions & 7 deletions src/components/TxSignView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@
</template>

<template v-else>
<tx-sign-summary
v-if="tx"
:tx="tx"
:assetInfo="assets"
:ownAddresses="addresses.map((a) => a.script)"
></tx-sign-summary>
<tx-sign-summary v-if="tx" :tx="tx" />

<div class="flex items-center pt-2">
<div class="flex-grow border-t border-gray-300 mx-2"></div>
Expand Down Expand Up @@ -163,7 +158,7 @@ export default defineComponent({
},
emits: ["success", "fail", "refused"],
props: {
transaction: { type: Object as PropType<Readonly<UnsignedTx | undefined>>, required: true },
transaction: { type: Object as PropType<Readonly<UnsignedTx>>, required: false },
inputsToSign: { type: Array<number>, required: false },
isModal: { type: Boolean, default: false },
setExternalState: {
Expand Down
4 changes: 2 additions & 2 deletions src/types/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,14 @@ export type UnsignedInput = {
index: number;
ergoTree: string;
creationHeight: number;
value: number | string;
value: string;
assets: Token[];
additionalRegisters: Registers;
extension: { [key: string]: string };
};

export type ErgoBoxCandidate = {
value: number | string;
value: string;
ergoTree: string;
creationHeight: number;
assets: Token[];
Expand Down

0 comments on commit 6e7c974

Please sign in to comment.