Skip to content

Commit

Permalink
fix: change would add invalid tokens on transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
r4mmer committed Dec 31, 2024
1 parent ab0b78e commit 49ad6d3
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 44 deletions.
38 changes: 32 additions & 6 deletions __tests__/integration/template/transaction/template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('Template execution', () => {
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', action: 'get_wallet_address' })
.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 })
Expand Down Expand Up @@ -191,18 +191,18 @@ describe('Template execution', () => {
expect(tx.outputs[1].value).toBe(2n);
});

/* eslint-disable jest/expect-expect */
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 })
.addSetVarAction({
name: 'tk_balance',
action: 'get_wallet_balance',
options: { token: '{token}' },
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();
Expand All @@ -213,14 +213,40 @@ describe('Template execution', () => {
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 () => {
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}' })
.addUtxoSelect({ fill: 100, token: '{token}', autoChange: false })
.addTokenOutput({ address: '{addr}', amount: 1, token: '{token}' })
.addDataOutput({ data: 'cafe', token: '{token}' })
.addChangeAction({})
Expand All @@ -232,6 +258,6 @@ describe('Template execution', () => {
const sendTx = new SendTransaction({ storage: hWallet.storage, transaction: tx });
await sendTx.runFromMining();
await waitForTxReceived(hWallet, tx.hash, null);
expect(tx.hash).toBeDefined();
});
/* eslint-enable jest/expect-expect */
});
5 changes: 2 additions & 3 deletions __tests__/template/transaction/instructions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,16 +759,15 @@ describe('should parse template instructions', () => {
SetVarInstruction.safeParse({
type: 'action/setvar',
name: 'foo',
action: 'get_wallet_address',
call: { method: 'get_wallet_address' },
}).success
).toBe(true);

expect(
SetVarInstruction.safeParse({
type: 'action/setvar',
name: 'foo',
action: 'get_wallet_balance',
options: { token },
call: { method: 'get_wallet_balance', token },
}).success
).toBe(true);
});
Expand Down
93 changes: 62 additions & 31 deletions src/template/transaction/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ export async function execDataOutputInstruction(
const { position, useCreatedToken } = ins;
const data = getVariable<string>(ins.data, ctx.vars, DataOutputInstruction.shape.data);
const token = getVariable<string>(ins.token, ctx.vars, DataOutputInstruction.shape.token);
ctx.log(`Creating data(${data}) output`);
ctx.log(`Creating data(${data}) output for token(${token})`);

let tokenData: number;
if (useCreatedToken) {
Expand Down Expand Up @@ -498,44 +498,57 @@ export async function execChangeInstruction(
} else {
// Check HTR and all tokens on the transaction
tokensToCheck.push(NATIVE_TOKEN_UID);
for (const tk in ctx.tokens) {
for (const tk of ctx.tokens) {
tokensToCheck.push(tk);
}
}

const script = createOutputScriptFromAddress(address, interpreter.getNetwork());

for (const tokenUid of tokensToCheck) {
ctx.log(`Checking change tx for token ${tokenUid}`);
const balance = ctx.balance.getTokenBalance(tokenUid);
const tokenData = ctx.addToken(tokenUid);
if (balance.tokens > 0) {
const value = balance.tokens;
ctx.log(`Adding a change output for ${value} / ${tokenUid}`);
// Need to create a token output
// Add balance to the ctx.balance
ctx.balance.addOutput(balance.tokens, tokenUid);
ctx.balance.addOutput(value, tokenUid);

// Creates an output with the value of the outstanding balance
const output = new Output(balance.tokens, script, { timelock, tokenData });
const output = new Output(value, script, { timelock, tokenData });
ctx.addOutput(-1, output);
}

if (balance.mint_authorities > 0) {
const count = balance.mint_authorities;
ctx.log(`Adding ${count} mint authority change outputs / ${tokenUid}`);
// Need to create a token output
// Add balance to the ctx.balance
ctx.balance.addOutputAuthority(balance.mint_authorities, tokenUid, 'mint');
ctx.balance.addOutputAuthority(count, tokenUid, 'mint');

// Creates an output with the value of the outstanding balance
const output = new Output(TOKEN_MINT_MASK, script, { timelock, tokenData });
ctx.addOutput(-1, ...Array(balance.mint_authorities).fill(output));
const output = new Output(TOKEN_MINT_MASK, script, {
timelock,
tokenData: tokenData | TOKEN_AUTHORITY_MASK,
});
ctx.addOutput(-1, ...Array(count).fill(output));
}

if (balance.melt_authorities > 0) {
const count = balance.mint_authorities;
ctx.log(`Adding ${count} melt authority change outputs / ${tokenUid}`);
// Need to create a token output
// Add balance to the ctx.balance
ctx.balance.addOutputAuthority(balance.melt_authorities, tokenUid, 'melt');
ctx.balance.addOutputAuthority(count, tokenUid, 'melt');

// Creates an output with the value of the outstanding balance
const output = new Output(TOKEN_MELT_MASK, script, { timelock, tokenData });
ctx.addOutput(-1, ...Array(balance.melt_authorities).fill(output));
const output = new Output(TOKEN_MELT_MASK, script, {
timelock,
tokenData: tokenData | TOKEN_AUTHORITY_MASK,
});
ctx.addOutput(-1, ...Array(count).fill(output));
}
}
}
Expand Down Expand Up @@ -594,21 +607,24 @@ export async function execCompleteTxInstruction(
const balance = ctx.balance.getTokenBalance(tokenUid);
const tokenData = ctx.addToken(tokenUid);
if (balance.tokens > 0) {
const value = balance.tokens;
// Surplus of token on the inputs, need to add a change output
ctx.log(`Creating a change output for ${balance.tokens}`);
ctx.log(`Creating a change output for ${value} / ${tokenUid}`);
// Add balance to the ctx.balance
ctx.balance.addOutput(balance.tokens, tokenUid);
ctx.balance.addOutput(value, tokenUid);

// Creates an output with the value of the outstanding balance
const output = new Output(balance.tokens, changeScript, { timelock, tokenData });
const output = new Output(value, changeScript, { timelock, tokenData });
ctx.addOutput(-1, output);
} else if (balance.tokens < 0) {
const value = -balance.tokens;
ctx.log(`Finding inputs for ${value} / ${tokenUid}`);
// Surplus of tokens on the outputs, need to select tokens and add inputs
const options: IGetUtxosOptions = { token: tokenUid };
if (address) {
options.filter_address = address;
}
const { changeAmount, utxos } = await interpreter.getUtxos(-balance.tokens, options);
const { changeAmount, utxos } = await interpreter.getUtxos(value, options);

// Add utxos as inputs on the transaction
const inputs: Input[] = [];
Expand Down Expand Up @@ -636,16 +652,23 @@ export async function execCompleteTxInstruction(
}

if (balance.mint_authorities > 0) {
const count = balance.mint_authorities;
ctx.log(`Creating ${count} mint outputs / ${tokenUid}`);
// Need to create a token output
// Add balance to the ctx.balance
ctx.balance.addOutputAuthority(balance.mint_authorities, tokenUid, 'mint');
ctx.balance.addOutputAuthority(count, tokenUid, 'mint');

// Creates an output with the value of the outstanding balance
const output = new Output(TOKEN_MINT_MASK, changeScript, { timelock, tokenData });
ctx.addOutput(-1, ...Array(balance.mint_authorities).fill(output));
const output = new Output(TOKEN_MINT_MASK, changeScript, {
timelock,
tokenData: tokenData | TOKEN_AUTHORITY_MASK,
});
ctx.addOutput(-1, ...Array(count).fill(output));
} else if (balance.mint_authorities < 0) {
const count = -balance.mint_authorities;
ctx.log(`Finding inputs for ${count} mint authorities / ${tokenUid}`);
// Need to find authorities to fill balance
const utxos = await interpreter.getAuthorities(-balance.mint_authorities, {
const utxos = await interpreter.getAuthorities(count, {
token: tokenUid,
authorities: 1n, // Mint
});
Expand All @@ -668,16 +691,23 @@ export async function execCompleteTxInstruction(
}

if (balance.melt_authorities > 0) {
const count = balance.melt_authorities;
ctx.log(`Creating ${count} melt outputs / ${tokenUid}`);
// Need to create a token output
// Add balance to the ctx.balance
ctx.balance.addOutputAuthority(balance.melt_authorities, tokenUid, 'melt');
ctx.balance.addOutputAuthority(count, tokenUid, 'melt');

// Creates an output with the value of the outstanding balance
const output = new Output(TOKEN_MELT_MASK, changeScript, { timelock, tokenData });
ctx.addOutput(-1, ...Array(balance.melt_authorities).fill(output));
const output = new Output(TOKEN_MELT_MASK, changeScript, {
timelock,
tokenData: tokenData | TOKEN_AUTHORITY_MASK,
});
ctx.addOutput(-1, ...Array(count).fill(output));
} else if (balance.melt_authorities < 0) {
const count = -balance.melt_authorities;
ctx.log(`Finding inputs for ${count} melt authorities / ${tokenUid}`);
// Need to find authorities to fill balance
const utxos = await interpreter.getAuthorities(-balance.melt_authorities, {
const utxos = await interpreter.getAuthorities(count, {
token: tokenUid,
authorities: 2n, // Melt
});
Expand Down Expand Up @@ -757,32 +787,33 @@ export async function execSetVarInstruction(
ins: z.infer<typeof SetVarInstruction>
) {
ctx.log(`Begin SetVarInstruction: ${JSONBigInt.stringify(ins)}`);
if (!ins.action) {
if (!ins.call) {
ctx.log(`Setting ${ins.name} with ${ins.value}`);
ctx.vars[ins.name] = ins.value;
return;
}

if (ins.action === 'get_wallet_address') {
if (ins.call.method === 'get_wallet_address') {
// Validate options and get token variable
const options = SetVarGetWalletAddressOpts.default({}).parse(ins.options);
const callArgs = SetVarGetWalletAddressOpts.parse(ins.call);
// Call action with valid options
const address = await getWalletAddress(interpreter, ctx, options);
const address = await getWalletAddress(interpreter, ctx, callArgs);
ctx.log(`Setting ${ins.name} with ${address}`);
ctx.vars[ins.name] = address;
return;
}
if (ins.action === 'get_wallet_balance') {
if (ins.call.method === 'get_wallet_balance') {
// Validate options and get token variable
const options = SetVarGetWalletBalanceOpts.default({}).parse(ins.options);
const callArgs = SetVarGetWalletBalanceOpts.parse(ins.call);
const token = getVariable<string>(
options.token,
callArgs.token,
ctx.vars,
SetVarGetWalletBalanceOpts.shape.token
);
options.token = token;
callArgs.token = token;
const newOptions = { ...callArgs, token };
// Call action with valid options
const balance = await getWalletBalance(interpreter, ctx, options);
const balance = await getWalletBalance(interpreter, ctx, newOptions);
ctx.vars[ins.name] = balance;
ctx.log(`Setting ${ins.name} with ${balance}`);
return;
Expand Down
10 changes: 7 additions & 3 deletions src/template/transaction/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,22 +189,26 @@ export const ConfigInstruction = z.object({
});

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 SetVarOptions = z.union([SetVarGetWalletAddressOpts, SetVarGetWalletBalanceOpts]);
export const SetVarCallArgs = z.discriminatedUnion('method', [
SetVarGetWalletAddressOpts,
SetVarGetWalletBalanceOpts,
]);

export const SetVarInstruction = z.object({
type: z.literal('action/setvar'),
name: z.string().regex(TEMPLATE_REFERENCE_NAME_RE),
value: z.any().optional(),
action: z.enum(['get_wallet_address', 'get_wallet_balance']).optional(),
options: SetVarOptions.optional(),
call: SetVarCallArgs.optional(),
});

export const TxTemplateInstruction = z.discriminatedUnion('type', [
Expand Down
3 changes: 2 additions & 1 deletion src/template/transaction/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export class WalletTxTemplateInterpreter implements ITxTemplateInterpreter {
}

async getBalance(token: string): Promise<IWalletBalanceData> {
return this.wallet.getBalance(token);
const balance = await this.wallet.getBalance(token);
return balance[0];
}

/**
Expand Down

0 comments on commit 49ad6d3

Please sign in to comment.