Skip to content

Commit f140252

Browse files
feat: add new authority actions for nano header
1 parent eee0c35 commit f140252

File tree

4 files changed

+181
-33
lines changed

4 files changed

+181
-33
lines changed

__tests__/integration/nanocontracts/bet.test.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ describe('full cycle of bet nano contract', () => {
142142
{
143143
type: 'deposit',
144144
token: NATIVE_TOKEN_UID,
145-
amount: 100,
145+
amount: 100n,
146146
changeAddress: address0,
147147
},
148148
],
@@ -158,7 +158,23 @@ describe('full cycle of bet nano contract', () => {
158158
{
159159
type: 'deposit',
160160
token: NATIVE_TOKEN_UID,
161-
amount: 100,
161+
amount: 100n,
162+
changeAddress: address0,
163+
},
164+
],
165+
})
166+
).rejects.toThrow(NanoContractTransactionError);
167+
168+
// Not enough funds for the deposit
169+
await expect(
170+
wallet.createAndSendNanoContractTransaction('bet', address2, {
171+
ncId: tx1.hash,
172+
args: [address2, '1x0'],
173+
actions: [
174+
{
175+
type: 'deposit',
176+
token: NATIVE_TOKEN_UID,
177+
amount: 1001n,
162178
changeAddress: address0,
163179
},
164180
],

src/nano_contracts/builder.ts

+111-13
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import {
1515
DEFAULT_TX_VERSION,
1616
NATIVE_TOKEN_UID,
1717
NANO_CONTRACTS_INITIALIZE_METHOD,
18+
TOKEN_MINT_MASK,
19+
TOKEN_MELT_MASK
1820
} from '../constants';
1921
import Serializer from './serializer';
2022
import HathorWallet from '../new/wallet';
21-
import { NanoContractTransactionError } from '../errors';
23+
import { NanoContractTransactionError, UtxoError } from '../errors';
2224
import {
2325
ActionTypeToActionHeaderType,
2426
NanoContractActionHeader,
@@ -196,18 +198,14 @@ class NanoContractTransactionBuilder {
196198
* Create inputs (and maybe change outputs) to complete the deposit
197199
*
198200
* @param {action} Action to be completed (must be a deposit type)
199-
* @param {tokens} Array of tokens to get the token data correctly
200201
*
201202
* @memberof NanoContractTransactionBuilder
202203
* @inner
203204
*/
204-
async executeDeposit(
205-
action: NanoContractAction,
206-
tokens: string[]
207-
): Promise<[IDataInput[], IDataOutput[]]> {
205+
async executeDeposit(action: NanoContractAction): Promise<[IDataInput[], IDataOutput[]]> {
208206
if (action.type !== NanoContractActionType.DEPOSIT) {
209207
throw new NanoContractTransactionError(
210-
"Can't execute a deposit with an action which type is differente than deposit."
208+
"Can't execute a deposit with an action which type is different than deposit."
211209
);
212210
}
213211

@@ -225,8 +223,17 @@ class NanoContractTransactionBuilder {
225223
if (action.address) {
226224
utxoOptions.filter_address = action.address;
227225
}
228-
const utxosData = await this.wallet.getUtxosForAmount(action.amount, utxoOptions);
229-
// XXX What if I don't have enough funds? Validate it!
226+
227+
let utxosData;
228+
try {
229+
utxosData = await this.wallet.getUtxosForAmount(action.amount, utxoOptions);
230+
} catch (e) {
231+
if (e instanceof UtxoError) {
232+
throw new NanoContractTransactionError('Not enough utxos to execute the deposit.');
233+
}
234+
235+
throw e;
236+
}
230237
const inputs: IDataInput[] = [];
231238
for (const utxo of utxosData.utxos) {
232239
inputs.push({
@@ -266,21 +273,20 @@ class NanoContractTransactionBuilder {
266273
* then creates the output only of the difference
267274
*
268275
* @param {action} Action to be completed (must be a withdrawal type)
269-
* @param {tokens} Array of tokens to get the token data correctly
270276
*
271277
* @memberof NanoContractTransactionBuilder
272278
* @inner
273279
*/
274-
executeWithdrawal(action: NanoContractAction, tokens: string[]): IDataOutput | null {
280+
executeWithdrawal(action: NanoContractAction): IDataOutput | null {
275281
if (action.type !== NanoContractActionType.WITHDRAWAL) {
276282
throw new NanoContractTransactionError(
277-
"Can't execute a withdrawal with an action which type is differente than withdrawal."
283+
"Can't execute a withdrawal with an action which type is different than withdrawal."
278284
);
279285
}
280286

281287
if (!action.amount || !action.token) {
282288
throw new NanoContractTransactionError(
283-
'Address, amount and token are required for withdrawal action.'
289+
'Amount and token are required for withdrawal action.'
284290
);
285291
}
286292

@@ -327,6 +333,98 @@ class NanoContractTransactionBuilder {
327333
};
328334
}
329335

336+
/**
337+
* Execute a grant authority action
338+
* Create inputs (and maybe change output) to complete the action
339+
*
340+
* @param {action} Action to be completed (must be a grant authority type)
341+
*
342+
* @memberof NanoContractTransactionBuilder
343+
* @inner
344+
*/
345+
async executeGrantAuthority(action: NanoContractAction): Promise<[IDataInput[], IDataOutput[]]> {
346+
if (action.type !== NanoContractActionType.GRANT_AUTHORITY) {
347+
throw new NanoContractTransactionError(
348+
"Can't execute a grant authority with an action which type is different than grant authority."
349+
);
350+
}
351+
352+
if (!action.authority || !action.token) {
353+
throw new NanoContractTransactionError('Authority and token are required for grant authority action.');
354+
}
355+
356+
const authorityAddressParam = action.authorityAddress;
357+
if (authorityAddressParam && !(await this.wallet.isAddressMine(authorityAddressParam))) {
358+
throw new NanoContractTransactionError('Authority address must belong to the same wallet.');
359+
}
360+
361+
// Get the utxos with the authority of the action and create the input
362+
const utxos = await this.wallet.getAuthorityUtxo(action.token, action.authority, { many: false, only_available_utxos: true, filter_address: action.address });
363+
364+
if (!utxos || utxos.length === 0) {
365+
throw new NanoContractTransactionError('Not enough authority utxos to execute the grant authority.');
366+
}
367+
368+
const inputs: IDataInput[] = [];
369+
// The method gets only one utxo
370+
const utxo = utxos[0];
371+
inputs.push({
372+
txId: utxo.txId,
373+
index: utxo.index,
374+
value: utxo.value,
375+
authorities: utxo.authorities,
376+
token: utxo.token,
377+
address: utxo.address,
378+
});
379+
380+
const outputs: IDataOutput[] = [];
381+
// If there's the authorityAddress param, then we must create another authority output for this address
382+
if (action.authorityAddress) {
383+
outputs.push({
384+
type: getAddressType(action.authorityAddress, this.wallet.getNetworkObject()),
385+
address: action.authorityAddress,
386+
value: action.authority === 'mint' ? TOKEN_MINT_MASK : TOKEN_MELT_MASK,
387+
timelock: null,
388+
token: action.token,
389+
authorities: action.authority === 'mint' ? 1n : 2n,
390+
});
391+
}
392+
393+
return [inputs, outputs];
394+
}
395+
396+
/**
397+
* Execute an invoke authority action
398+
*
399+
* @param {action} Action to be completed (must be an invoke authority type)
400+
*
401+
* @memberof NanoContractTransactionBuilder
402+
* @inner
403+
*/
404+
executeInvokeAuthority(action: NanoContractAction): IDataOutput | null {
405+
if (action.type !== NanoContractActionType.INVOKE_AUTHORITY) {
406+
throw new NanoContractTransactionError(
407+
"Can't execute an invoke authority with an action which type is different than invoke authority."
408+
);
409+
}
410+
411+
if (!action.address || !action.authority || !action.token) {
412+
throw new NanoContractTransactionError(
413+
'Address, authority, and token are required for invoke authority action.'
414+
);
415+
}
416+
417+
// Create the output with the authority of the action
418+
return {
419+
type: getAddressType(action.address, this.wallet.getNetworkObject()),
420+
address: action.address,
421+
value: action.authority === 'mint' ? TOKEN_MINT_MASK : TOKEN_MELT_MASK,
422+
timelock: null,
423+
token: action.token,
424+
authorities: action.authority === 'mint' ? 1n : 2n,
425+
};
426+
}
427+
330428
/**
331429
* Verify if the builder attributes are valid for the nano build
332430
*

src/nano_contracts/types.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,15 @@ export enum NanoContractVertexType {
2323
export enum NanoContractActionType {
2424
DEPOSIT = 'deposit',
2525
WITHDRAWAL = 'withdrawal',
26+
GRANT_AUTHORITY = 'grant_authority',
27+
INVOKE_AUTHORITY = 'invoke_authority'
2628
}
2729

2830
export enum NanoContractHeaderActionType {
2931
DEPOSIT = 1,
3032
WITHDRAWAL = 2,
33+
GRANT_AUTHORITY = 3,
34+
INVOKE_AUTHORITY = 4,
3135
}
3236

3337
export const ActionTypeToActionHeaderType: Record<
@@ -36,6 +40,8 @@ export const ActionTypeToActionHeaderType: Record<
3640
> = {
3741
[NanoContractActionType.DEPOSIT]: NanoContractHeaderActionType.DEPOSIT,
3842
[NanoContractActionType.WITHDRAWAL]: NanoContractHeaderActionType.WITHDRAWAL,
43+
[NanoContractActionType.GRANT_AUTHORITY]: NanoContractHeaderActionType.GRANT_AUTHORITY,
44+
[NanoContractActionType.INVOKE_AUTHORITY]: NanoContractHeaderActionType.INVOKE_AUTHORITY,
3945
};
4046

4147
// The action in the header is serialized/deserialized in the class
@@ -50,12 +56,18 @@ export interface NanoContractActionHeader {
5056
export interface NanoContractAction {
5157
type: NanoContractActionType;
5258
token: string;
53-
amount: OutputValueType;
54-
// For withdrawal is optional, which is address to send the output, in case one is created
55-
// For deposit is optional, and it's the address to filter the utxos
59+
// For withdrawal/deposit is required but authority actions
60+
// will receive its information from the authority field
61+
amount: OutputValueType | null;
62+
// For withdrawal and create authority is required, which is address to send the output
63+
// For deposit or grant authority actions is optional, and it's the address to filter the utxos
5664
address: string | null;
5765
// For deposit action is the change address used by the change output after selecting the utxos
5866
changeAddress: string | null;
67+
// In case of an authority action, it specifies which authority
68+
authority: 'mint' | 'melt' | null;
69+
// For grant authority action, it's the address to create the authority output, if the user wants to keep it
70+
authorityAddress: string | null;
5971
}
6072

6173
// Arguments for blueprint methods

src/new/wallet.js

+37-15
Original file line numberDiff line numberDiff line change
@@ -1798,6 +1798,7 @@ class HathorWallet extends EventEmitter {
17981798
* @param {Object} [options] Object with custom options.
17991799
* @param {boolean} [options.many=false] if should return many utxos or just one (default false)
18001800
* @param {boolean} [options.only_available_utxos=false] If we should filter for available utxos.
1801+
* @param {string} [options.filter_address=null] Address to filter the utxo to get.
18011802
*
18021803
* @return {Promise<{
18031804
* txId: string,
@@ -1809,20 +1810,7 @@ class HathorWallet extends EventEmitter {
18091810
* Returns an empty array in case there are no tx_outupts for this type.
18101811
* */
18111812
async getMintAuthority(tokenUid, options = {}) {
1812-
const newOptions = {
1813-
token: tokenUid,
1814-
authorities: 1n, // mint authority
1815-
only_available_utxos: options.only_available_utxos ?? false,
1816-
};
1817-
if (!options.many) {
1818-
// limit number of utxos to select if many is false
1819-
newOptions.max_utxos = 1;
1820-
}
1821-
const utxos = [];
1822-
for await (const utxo of this.storage.selectUtxos(newOptions)) {
1823-
utxos.push(utxo);
1824-
}
1825-
return utxos;
1813+
return this.getAuthorityUtxo(tokenUid, 'mint', options);
18261814
}
18271815

18281816
/**
@@ -1832,6 +1820,7 @@ class HathorWallet extends EventEmitter {
18321820
* @param {Object} [options] Object with custom options.
18331821
* @param {boolean} [options.many=false] if should return many utxos or just one (default false)
18341822
* @param {boolean} [options.only_available_utxos=false] If we should filter for available utxos.
1823+
* @param {string} [options.filter_address=null] Address to filter the utxo to get.
18351824
*
18361825
* @return {Promise<{
18371826
* txId: string,
@@ -1843,10 +1832,43 @@ class HathorWallet extends EventEmitter {
18431832
* Returns an empty array in case there are no tx_outupts for this type.
18441833
* */
18451834
async getMeltAuthority(tokenUid, options = {}) {
1835+
return this.getAuthorityUtxo(tokenUid, 'melt', options);
1836+
}
1837+
1838+
/**
1839+
* Get authority utxo
1840+
*
1841+
* @param {string} tokenUid UID of the token to select the authority utxo
1842+
* @param {string} authority The authority to filter ('mint' or 'melt')
1843+
* @param {Object} [options] Object with custom options.
1844+
* @param {boolean} [options.many=false] if should return many utxos or just one (default false)
1845+
* @param {boolean} [options.only_available_utxos=false] If we should filter for available utxos.
1846+
* @param {string} [options.filter_address=null] Address to filter the utxo to get.
1847+
*
1848+
* @return {Promise<{
1849+
* txId: string,
1850+
* index: number,
1851+
* address: string,
1852+
* authorities: OutputValueType
1853+
* }[]>} Promise that resolves with an Array of objects with properties of the authority output.
1854+
* The "authorities" field actually contains the output value with the authority masks.
1855+
* Returns an empty array in case there are no tx_outupts for this type.
1856+
* */
1857+
async getAuthorityUtxo(tokenUid, authority, options = {}) {
1858+
let authorityValue;
1859+
if (authority === 'mint') {
1860+
authorityValue = 1n;
1861+
} else if (authority === 'melt') {
1862+
authorityValue = 2n;
1863+
} else {
1864+
throw new Error('Invalid authority value.');
1865+
}
1866+
18461867
const newOptions = {
18471868
token: tokenUid,
1848-
authorities: 2n, // melt authority
1869+
authorities: authorityValue,
18491870
only_available_utxos: options.only_available_utxos ?? false,
1871+
filter_address: options.filter_address ?? null,
18501872
};
18511873
if (!options.many) {
18521874
// limit number of utxos to select if many is false

0 commit comments

Comments
 (0)