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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cbb5490
feat: POC implementation
r4mmer Nov 11, 2024
9755aa8
feat: initial implementation
r4mmer Nov 11, 2024
a342ab7
feat: add support for create token transaction
r4mmer Dec 16, 2024
edb3894
feat: add balance management for created token reference
r4mmer Dec 17, 2024
250105d
tests: instruction tests
r4mmer Dec 20, 2024
90dab94
feat: complete instruction
r4mmer Dec 23, 2024
13efa80
feat: complete instruction
r4mmer Dec 23, 2024
fec87f6
feat: executor tests
r4mmer Dec 26, 2024
fff192b
tests: executor and interpreter tests
r4mmer Dec 26, 2024
d46c571
tests(integration): custom token integration template tests
r4mmer Dec 26, 2024
6f91f9e
chore: linter changes
r4mmer Dec 27, 2024
3a3b9ea
chore: linter changes
r4mmer Dec 27, 2024
30f1764
tests(integration): change injectFunds options
r4mmer Dec 27, 2024
3b2ae37
tests(integration): create token tx have different serialization
r4mmer Dec 27, 2024
7099194
tests(integration): include change outputs in checks
r4mmer Dec 27, 2024
edd8234
tests(integration): change output orderr
r4mmer Dec 27, 2024
d120896
tests(integration): add change value to send operation
r4mmer Dec 27, 2024
a50620a
chore: remove helper types to use inferred types from zod
r4mmer Dec 27, 2024
ee5edf4
tests(integration): add change and complete tests
r4mmer Dec 27, 2024
ab0b78e
tests(integration): wrong complete instruction type
r4mmer Dec 30, 2024
49ad6d3
fix: change would add invalid tokens on transaction
r4mmer Dec 31, 2024
674aa2d
tests(unit): mock balance properly
r4mmer Dec 31, 2024
c7e968c
feat: expose transaction template and helper method on the wallet
r4mmer Jan 7, 2025
61b9502
chore: linter changes
r4mmer Jan 8, 2025
ba4110d
test(integration): increase coverage on integration tests
r4mmer Jan 8, 2025
1d8cb3f
tests(integration): change position of tests
r4mmer Jan 8, 2025
b2cd834
tests(integration): change tx version
r4mmer Jan 8, 2025
d82aa19
feat: make tx signing optional when building with the facade
r4mmer Jan 9, 2025
33591c3
feat: correctly pass the arguments to build template
r4mmer Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion __tests__/integration/hathorwallet_facade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,62 @@ import Network from '../../src/models/network';
import { WalletType } from '../../src/types';
import { parseScriptData } from '../../src/utils/scripts';
import { MemoryStore, Storage } from '../../src/storage';
import { TransactionTemplateBuilder } from '../../src/template/transaction';

const fakeTokenUid = '008a19f84f2ae284f19bf3d03386c878ddd15b8b0b604a3a3539aa9d714686e1';
const sampleNftData =
'ipfs://bafybeiccfclkdtucu6y4yc5cpr6y3yuinr67svmii46v5cfcrkp47ihehy/albums/QXBvbGxvIDEwIE1hZ2F6aW5lIDI3L04=/21716695748_7390815218_o.jpg';

describe('template methods', () => {
afterEach(async () => {
await stopAllWallets();
await GenesisWalletHelper.clearListeners();
});

it('should build transactions from the template transaction', async () => {
const hWallet = await generateWalletHelper();
const address = await hWallet.getAddressAtIndex(1);

await GenesisWalletHelper.injectFunds(hWallet, address, 10n);

const template = new TransactionTemplateBuilder()
.addConfigAction({ tokenName: 'Tmpl Token', tokenSymbol: 'TT' })
.addSetVarAction({ name: 'addr', call: { method: 'get_wallet_address' } })
.addUtxoSelect({ fill: 1 })
.addTokenOutput({ address: '{addr}', amount: 100, useCreatedToken: true })
Comment on lines +58 to +59
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 })

.build();

const tx = await hWallet.buildTxTemplate(template, { signTx: true, pinCode: DEFAULT_PIN_CODE });
expect(tx.version).toEqual(2); // Create token transaction
expect(tx.inputs).toHaveLength(1);
expect(tx.inputs[0].data).not.toBeFalsy(); // Tx is signed
// Transaction is not mined yet
expect(tx.hash).toBeNull();
expect(tx.nonce).toEqual(0);
});

it('should send transactions from the template transaction', async () => {
const hWallet = await generateWalletHelper();
const address = await hWallet.getAddressAtIndex(1);

await GenesisWalletHelper.injectFunds(hWallet, address, 10n);

const template = new TransactionTemplateBuilder()
.addConfigAction({ tokenName: 'Tmpl Token', tokenSymbol: 'TT' })
.addSetVarAction({ name: 'addr', call: { method: 'get_wallet_address' } })
.addUtxoSelect({ fill: 1 })
.addTokenOutput({ address: '{addr}', amount: 100, useCreatedToken: true })
.build();

const tx = await hWallet.runTxTemplate(template, DEFAULT_PIN_CODE);
expect(tx.version).toEqual(2); // Create token transaction
expect(tx.inputs).toHaveLength(1);
expect(tx.inputs[0].data).not.toBeFalsy(); // Tx is signed
// Transaction is mined and pushed
expect(tx.hash).not.toBeNull();
});
});

describe('getWalletInputInfo', () => {
afterEach(async () => {
await stopAllWallets();
Expand Down Expand Up @@ -143,7 +194,7 @@ describe('getTxById', () => {
},
});
await expect(hWallet.getTxById(tx1.hash)).rejects.toThrow(
'Token undefined not found in tokens list'
'Invalid token_data undefined, token not found in tokens list'
);
jest.spyOn(hWallet, 'getFullTxById').mockRestore();

Expand Down
263 changes: 263 additions & 0 deletions __tests__/integration/template/transaction/template.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import { GenesisWalletHelper } from '../../helpers/genesis-wallet.helper';
import {
DEFAULT_PIN_CODE,
generateWalletHelper,
stopAllWallets,
waitForTxReceived,
} from '../../helpers/wallet.helper';

import HathorWallet from '../../../../src/new/wallet';
import SendTransaction from '../../../../src/new/sendTransaction';
import transactionUtils from '../../../../src/utils/transaction';
import { TransactionTemplateBuilder } from '../../../../src/template/transaction/builder';
import { WalletTxTemplateInterpreter } from '../../../../src/template/transaction/interpreter';

const DEBUG = true;

describe('Template execution', () => {
let hWallet: HathorWallet;
let interpreter: WalletTxTemplateInterpreter;
let tokenUid: string;

beforeAll(async () => {
hWallet = await generateWalletHelper(null);
interpreter = new WalletTxTemplateInterpreter(hWallet);
const address = await hWallet.getAddressAtIndex(0);
await GenesisWalletHelper.injectFunds(hWallet, address, 10n, {});
});

afterAll(async () => {
await hWallet.stop();
await stopAllWallets();
await GenesisWalletHelper.clearListeners();
});

it('should be able to create a custom token', async () => {
const template = new TransactionTemplateBuilder()
.addConfigAction({ tokenName: 'Tmpl Test Token 01', tokenSymbol: 'TTT01' })
.addSetVarAction({ name: 'addr', call: { method: 'get_wallet_address' } })
.addUtxoSelect({ fill: 1 })
.addTokenOutput({ address: '{addr}', amount: 100, useCreatedToken: true })
.addAuthorityOutput({ authority: 'mint', address: '{addr}', useCreatedToken: true, count: 5 })
.addAuthorityOutput({ authority: 'melt', address: '{addr}', useCreatedToken: true, count: 5 })
.build();

const tx = await interpreter.build(template, DEBUG);
await transactionUtils.signTransaction(tx, hWallet.storage, DEFAULT_PIN_CODE);
tx.prepareToSend();
const sendTx = new SendTransaction({ storage: hWallet.storage, transaction: tx });
await sendTx.runFromMining();
if (tx.hash === null) {
throw new Error('Transaction does not have a hash');
}
tokenUid = tx.hash;
await waitForTxReceived(hWallet, tx.hash, null);

expect(tx.outputs).toHaveLength(12);

// HTR change
expect(tx.outputs[0].tokenData).toBe(0);
expect(tx.outputs[0].value).toBe(9n);

// Created token
expect(tx.outputs[1].tokenData).toBe(1);
expect(tx.outputs[1].value).toBe(100n);

// 5 mint authorities
expect(tx.outputs[2].tokenData).toBe(129);
expect(tx.outputs[2].value).toBe(1n);
expect(tx.outputs[6].tokenData).toBe(129);
expect(tx.outputs[6].value).toBe(1n);

// 5 melt authorities
expect(tx.outputs[7].tokenData).toBe(129);
expect(tx.outputs[7].value).toBe(2n);
expect(tx.outputs[11].tokenData).toBe(129);
expect(tx.outputs[11].value).toBe(2n);
});

it('should be able to send tokens and authorities', async () => {
const address = await hWallet.getAddressAtIndex(10);
const template = new TransactionTemplateBuilder()
.addSetVarAction({ name: 'addr', value: address })
.addSetVarAction({ name: 'token', value: tokenUid })
.addUtxoSelect({ fill: 2 })
.addTokenOutput({ address: '{addr}', amount: 2 })
.addUtxoSelect({ fill: 3, token: '{token}' })
.addTokenOutput({ address: '{addr}', amount: 3, token: '{token}' })
.addAuthoritySelect({ authority: 'mint', token: '{token}', count: 1 })
.addAuthorityOutput({ address: '{addr}', authority: 'mint', count: 1, token: '{token}' })
.addAuthoritySelect({ authority: 'melt', token: '{token}', count: 2 })
.addAuthorityOutput({ address: '{addr}', authority: 'melt', count: 2, token: '{token}' })
.build();

const tx = await interpreter.build(template, DEBUG);
await transactionUtils.signTransaction(tx, hWallet.storage, DEFAULT_PIN_CODE);
tx.prepareToSend();
const sendTx = new SendTransaction({ storage: hWallet.storage, transaction: tx });
await sendTx.runFromMining();
await waitForTxReceived(hWallet, tx.hash, null);

expect(tx.outputs).toHaveLength(7);

// HTR Change
expect(tx.outputs[0].tokenData).toBe(0);
expect(tx.outputs[0].value).toBe(7n);

// HTR
expect(tx.outputs[1].tokenData).toBe(0);
expect(tx.outputs[1].value).toBe(2n);

// Custom token change
expect(tx.outputs[2].tokenData).toBe(1);
expect(tx.outputs[2].value).toBe(97n);

// Custom token
expect(tx.outputs[3].tokenData).toBe(1);
expect(tx.outputs[3].value).toBe(3n);

// mint authority
expect(tx.outputs[4].tokenData).toBe(129);
expect(tx.outputs[4].value).toBe(1n);

// melt authorities
expect(tx.outputs[5].tokenData).toBe(129);
expect(tx.outputs[5].value).toBe(2n);
expect(tx.outputs[6].tokenData).toBe(129);
expect(tx.outputs[6].value).toBe(2n);
});

it('should be able to destroy authorities', async () => {
const template = new TransactionTemplateBuilder()
.addSetVarAction({ name: 'token', value: tokenUid })
.addAuthoritySelect({ authority: 'mint', token: '{token}', count: 4 })
.addAuthoritySelect({ authority: 'melt', token: '{token}', count: 4 })
.build();

const tx = await interpreter.build(template, DEBUG);
await transactionUtils.signTransaction(tx, hWallet.storage, DEFAULT_PIN_CODE);
tx.prepareToSend();
const sendTx = new SendTransaction({ storage: hWallet.storage, transaction: tx });
await sendTx.runFromMining();
await waitForTxReceived(hWallet, tx.hash, null);

expect(tx.outputs).toHaveLength(0);
});

it('should be able to mint new tokens', async () => {
const address = await hWallet.getAddressAtIndex(15);
const template = new TransactionTemplateBuilder()
.addSetVarAction({ name: 'addr', value: address })
.addSetVarAction({ name: 'token', value: tokenUid })
.addUtxoSelect({ fill: 1 })
.addAuthoritySelect({ authority: 'mint', token: '{token}' })
.addTokenOutput({ address: '{addr}', amount: 100, token: '{token}' })
.addAuthorityOutput({ address: '{addr}', authority: 'mint', token: '{token}' })
.build();

const tx = await interpreter.build(template, DEBUG);
await transactionUtils.signTransaction(tx, hWallet.storage, DEFAULT_PIN_CODE);
tx.prepareToSend();
const sendTx = new SendTransaction({ storage: hWallet.storage, transaction: tx });
await sendTx.runFromMining();
await waitForTxReceived(hWallet, tx.hash, null);

expect(tx.outputs[0].tokenData).toBe(0);
expect(tx.outputs[1].tokenData).toBe(1);
expect(tx.outputs[1].value).toBe(100n);
});

it('should be able to melt tokens', async () => {
const address = await hWallet.getAddressAtIndex(20);
const template = new TransactionTemplateBuilder()
.addSetVarAction({ name: 'addr', value: address })
.addSetVarAction({ name: 'token', value: tokenUid })
.addUtxoSelect({ fill: 100, token: '{token}' })
.addAuthoritySelect({ authority: 'melt', token: '{token}' })
.addTokenOutput({ address: '{addr}', amount: 1 })
.addAuthorityOutput({ address: '{addr}', authority: 'melt', token: '{token}' })
.build();

const tx = await interpreter.build(template, DEBUG);
await transactionUtils.signTransaction(tx, hWallet.storage, DEFAULT_PIN_CODE);
tx.prepareToSend();
const sendTx = new SendTransaction({ storage: hWallet.storage, transaction: tx });
await sendTx.runFromMining();
await waitForTxReceived(hWallet, tx.hash, null);

expect(tx.outputs[0].tokenData).toBe(0);
expect(tx.outputs[0].value).toBe(1n);
expect(tx.outputs[1].tokenData).toBe(129);
expect(tx.outputs[1].value).toBe(2n);
});

it('should be able to complete a transaction inputs', async () => {
const address = await hWallet.getAddressAtIndex(25);
const template = new TransactionTemplateBuilder()
.addSetVarAction({ name: 'addr', value: address })
.addSetVarAction({ name: 'token', value: tokenUid })
Comment on lines +197 to +198
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.

.addSetVarAction({
name: 'tk_balance',
call: { method: 'get_wallet_balance', token: '{token}' },
})
.addTokenOutput({ address: '{addr}', amount: '{tk_balance}', token: '{token}' })
.addAuthorityOutput({ address: '{addr}', authority: 'mint', token: '{token}' })
.addAuthorityOutput({ address: '{addr}', authority: 'mint', token: '{token}' })
.addCompleteAction({})
.addShuffleAction({ target: 'all' })
.build();

const tx = await interpreter.build(template, DEBUG);
await transactionUtils.signTransaction(tx, hWallet.storage, DEFAULT_PIN_CODE);
tx.prepareToSend();
const sendTx = new SendTransaction({ storage: hWallet.storage, transaction: tx });
await sendTx.runFromMining();
await waitForTxReceived(hWallet, tx.hash, null);
expect(tx.hash).toBeDefined();
});

it('should be able to complete a transaction change', async () => {
const address = await hWallet.getAddressAtIndex(25);
const template = new TransactionTemplateBuilder()
.addSetVarAction({ name: 'addr', value: address })
.addSetVarAction({ name: 'token', value: tokenUid })
.addSetVarAction({
name: 'tk_balance',
call: { method: 'get_wallet_balance', token: '{token}' },
})
.addUtxoSelect({ fill: '{tk_balance}', token: '{token}', autoChange: false })
.addAuthoritySelect({ token: '{token}', authority: 'mint' })
.addAuthoritySelect({ token: '{token}', authority: 'melt' })
.addTokenOutput({ address: '{addr}', amount: 1, token: '{token}' })
.addCompleteAction({})
.build();

const tx = await interpreter.build(template, DEBUG);
await transactionUtils.signTransaction(tx, hWallet.storage, DEFAULT_PIN_CODE);
tx.prepareToSend();
const sendTx = new SendTransaction({ storage: hWallet.storage, transaction: tx });
await sendTx.runFromMining();
await waitForTxReceived(hWallet, tx.hash, null);
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.

const address = await hWallet.getAddressAtIndex(25);
const template = new TransactionTemplateBuilder()
.addSetVarAction({ name: 'addr', value: address })
.addSetVarAction({ name: 'token', value: tokenUid })
.addUtxoSelect({ fill: 100, token: '{token}', autoChange: false })
.addTokenOutput({ address: '{addr}', amount: 1, token: '{token}' })
.addDataOutput({ data: 'cafe', token: '{token}' })
.addChangeAction({})
.build();

const tx = await interpreter.build(template, DEBUG);
await transactionUtils.signTransaction(tx, hWallet.storage, DEFAULT_PIN_CODE);
tx.prepareToSend();
const sendTx = new SendTransaction({ storage: hWallet.storage, transaction: tx });
await sendTx.runFromMining();
await waitForTxReceived(hWallet, tx.hash, null);
expect(tx.hash).toBeDefined();
});
});
Loading
Loading