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

feat: transaction template #808

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open

feat: transaction template #808

wants to merge 29 commits into from

Conversation

r4mmer
Copy link
Member

@r4mmer r4mmer commented Dec 13, 2024

Acceptance Criteria

  • Implement the transaction template design (reference)

Code organization

The transaction template code is organized in the following modules:

  • instructions
    • Parsers and definitions of the user input and the formatted data that will be used as transaction templates.
  • executor
    • Methods that will determine how each of the instructions work when running a template.
  • setvarcommands
    • sub-routines to execute with the action/setvar instructions.
  • interpreter
    • Wallet facade interpreter, acts as the bridge between the template and the wallet (fetching utxos, txs, balance.)
  • context
    • State of the template execution and tracking of data (for instance, the balance of the transaction being built).
  • builder
    • A helper class to build a transaction template.
  • types
    • Typing to add abstraction.
    • This is required to split the modules in different files, without this we would introduce some import cycles.

Note

The interpreter for the wallet-service is not part of this implementation.

Note

There are some differences between the design and the instructions on the implementation but this is because of a "not-intuitive-behavior" on zod, specially when dealing with unions.

Future possibilities

  1. Code duplication in the executors

In the executors there is some code duplication (especially when creating inputs/outputs and updating the context balance), but I avoided abstracting too much in the initial implementation so we can have more freedom in how we implement each instruction, later I think we should come back to this and make the executor code cleaner.

  1. Choosing change address

An issue in the usual transactions of the wallet facade is that if we get a wallet address to use and then a change address it can be the same address since the wallet does not have the context of other addresses that are being used in the transaction.
The template has this information and it could be used to avoid choosing the same address twice in the same tx.
Since this is not an issue with the wallet facade I did not implement in the tx template but we could easily add this.

@r4mmer r4mmer self-assigned this Dec 13, 2024
Copy link

codecov bot commented Dec 13, 2024

Codecov Report

Attention: Patch coverage is 81.23953% with 112 lines in your changes missing coverage. Please review.

Project coverage is 80.75%. Comparing base (c06d3ce) to head (33591c3).

Files with missing lines Patch % Lines
src/template/transaction/executor.ts 84.36% 53 Missing ⚠️
src/template/transaction/interpreter.ts 63.26% 18 Missing ⚠️
src/template/transaction/builder.ts 75.86% 14 Missing ⚠️
src/utils/transaction.ts 0.00% 14 Missing ⚠️
src/template/transaction/context.ts 87.34% 10 Missing ⚠️
src/template/transaction/setvarcommands.ts 62.50% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #808      +/-   ##
==========================================
+ Coverage   80.68%   80.75%   +0.07%     
==========================================
  Files          85       91       +6     
  Lines        6549     7145     +596     
  Branches     1419     1489      +70     
==========================================
+ Hits         5284     5770     +486     
- Misses       1252     1362     +110     
  Partials       13       13              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@r4mmer r4mmer requested a review from tuliomir January 2, 2025 14:39
@@ -2781,7 +2782,7 @@ class HathorWallet extends EventEmitter {
const tokenIdx = tokenUtils.getTokenIndexFromData(token_data);
const tokenUid = tokens[tokenIdx - 1]?.uid;
if (!tokenUid) {
throw new Error(`Token ${tokenUid} not found in tokens list`);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if !tokenUid means that tokenUid is undefined or empty string, which makes no sense adding to the error message.
This is why I changed to token_data instead, this makes understanding what went wrong easier.

this.template = [];
}

static from(instructions: z.input<typeof TransactionTemplate>): TransactionTemplateBuilder {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obs: z.input<T> means that this accepts the "unparsed" instruction.
When the type is z.infer<T> it means the output of the parsed data.

Comment on lines +191 to +205
export const SetVarGetWalletAddressOpts = z.object({
method: z.literal('get_wallet_address'),
index: z.number().optional(),
});

export const SetVarGetWalletBalanceOpts = z.object({
method: z.literal('get_wallet_balance'),
token: TemplateRef.or(TokenSchema.default('00')),
authority: z.enum(['mint', 'melt']).optional(),
});

export const SetVarCallArgs = z.discriminatedUnion('method', [
SetVarGetWalletAddressOpts,
SetVarGetWalletBalanceOpts,
]);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was changed from the design because non discriminated unions with a lot of optionals were being difficult to parse correctly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explicit what was changed to facilitate a posterior update of the design?

Comment on lines +58 to +59
.addUtxoSelect({ fill: 1 })
.addTokenOutput({ address: '{addr}', amount: 100, useCreatedToken: true })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Should we enforce these parameters to be bigints?

Suggested change
.addUtxoSelect({ fill: 1 })
.addTokenOutput({ address: '{addr}', amount: 100, useCreatedToken: true })
.addUtxoSelect({ fill: 1n })
.addTokenOutput({ address: '{addr}', amount: 100n, useCreatedToken: true })


outputs: Output[];

tokens: string[]; // use token data?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: What were your ideas for this comment?

At first glance it seems to me that it's better to keep a separate array to manage the context that does not involve the actual Transaction object.

tokenSymbol?: string;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
vars: Record<string, any>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(non-blocking): By using unknown, each call to the vars need to have its type cast, which helps with the type safety and removes the warning.

Suggested change
vars: Record<string, any>;
vars: Record<string, unknown>;

this.version = CREATE_TOKEN_TX_VERSION;
}

getTokenDataFor(token: string, useCreatedToken: boolean) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chore: Change the name of the method to better communicate its purpose. As of now, it is a get method that actually mutates the object and adds a new token.

Also, when useCreatedToken is passed, token is discarded, which is also slightly confusing and would be better accompanied by a method docstring to explain it.

return this.tokens.length;
}

addInput(position: number, ...inputs: Input[]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chore: Change the name to better inform that multiple inputs can be added, and that the expected parameter is actually an array and not an object. Initial suggestions would be addInputs or addManyInputs.

Comment on lines +305 to +308
ctx.log(`amount(${amount})`);
if (!amount) {
throw new Error('Raw token output missing amount');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(non-blocking): Move this validation closer to line 263, so that this quick and mandatory validation can fail early.

ctx.addOutput(-1, output);
}

if (balance.mint_authorities > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought(non-blocking): It feels weird having change addresses for authorities, since each UTXO can only hold a single authority.

I understand this as a user-friendly way of dealing with the transaction having more authority inputs than needed. Not sure if this should have a comment or any kind of feedback/warning for the user.

Comment on lines +197 to +198
.addSetVarAction({ name: 'addr', value: address })
.addSetVarAction({ name: 'token', value: tokenUid })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chore: Not related to these lines, but all tests made here use variable values. At least one of them should test passing the address/value directly to the instructions.

expect(tx.hash).toBeDefined();
});

it('should be able to complete change', async () => {
Copy link
Contributor

@tuliomir tuliomir Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chore: The title is not clear about the difference between this test and the last one.

ctx.addInput(-1, new Input(txId, Number(i)));
arr.push(i);
}
await executor(interpreter, ctx, ins);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(non-blocking): Add assertions that ensure the steps are the same with a method like contains, or check the final balance of the transaction to confirm its equivalence to the original one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Review (WIP)
Development

Successfully merging this pull request may close these issues.

2 participants