Skip to content

Commit

Permalink
feat: nano contract integration
Browse files Browse the repository at this point in the history
feat: add nano APIs to get state and history

feat: add nano contract class and serialization class

feat: add methods to fully integrate bet blueprint with the wallet class

tests: integration tests should run only after the hathor core docker image supports nano

tests: add comments about skipped integration test

docs: add docstring to utils methods

docs: add docstring to nc builder methods

refactor: reuse code through nano contract methods and create error class to throw specific errors

docs: improve new methods documentation

refactor: create a class for the bet blueprint with each method implementation and a generic method for the wallet class

feat: add new nano API fields for state and history

feat: create nano contract builder to improve integration

fix: should check args types only if there are args

tests: update integration tests for bet with new APIs parameters and NC builder

feat: add support for custom token in actions

chore: remove unused import

fix: call getHDPrivateKeyFromAddress with options as second param (#592)

This function extracts the pin from options or get the default value
from the wallet.

The current implementation causes an error in the wallet-mobile because
the wallet doesn't have a default pin.

feat: add get blueprint information api

feat: add nano contract to the tx version

feat: add nano contract parser and deserializer

refactor: remove argument types in the builder and improve actions typing

feat: add error handling to nano contract tx parse

refactor: getOracleInputData now receives oracle data as buffer

refactor: execute nc method now checkes the argument types of each parameter validating with the full node blueprint API

tests: adapt integration tests for new nano method parameters

feat: add support for parsing str and signed data

fix: fix bytes handling for signed fields

docs: add docstring to nano utils methods and add its methods to the lib.ts file

tests: refactor in test to use new method name and nano serializer in the result
  • Loading branch information
pedroferreira1 committed Jan 19, 2024
1 parent 80102a5 commit 54b250c
Show file tree
Hide file tree
Showing 21 changed files with 1,558 additions and 42 deletions.
6 changes: 4 additions & 2 deletions __tests__/integration/configuration/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ services:
# https://github.com/HathorNetwork/rfcs/blob/master/text/0033-private-network-guide.md

fullnode:
image: hathornetwork/hathor-core
image:
${HATHOR_LIB_INTEGRATION_TESTS_FULLNODE_IMAGE:-hathornetwork/hathor-core}
command: [
"run_node",
"--listen", "tcp:40404",
Expand All @@ -29,7 +30,8 @@ services:
- hathor-privnet

tx-mining-service:
image: hathornetwork/tx-mining-service
image:
${HATHOR_LIB_INTEGRATION_TESTS_TXMINING_IMAGE:-hathornetwork/tx-mining-service}
depends_on:
- fullnode
ports:
Expand Down
47 changes: 47 additions & 0 deletions __tests__/integration/helpers/wallet.helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { delay } from '../utils/core.util';
import { loggers } from '../utils/logger.util';
import { MemoryStore, Storage } from '../../../src/storage';
import { TxHistoryProcessingStatus } from '../../../src/types';
import { get } from 'lodash';

/**
* @typedef SendTxResponse
Expand Down Expand Up @@ -437,3 +438,49 @@ export async function waitNextBlock(storage) {
throw new Error('Timeout reached when waiting for the next block.');
}
}

/**
* This method awaits a tx to be confirmed by a block and then resolves the promise.
*
* It does not return any content, only delivers the code processing back to the caller at the
* desired time.
*
* @param {HathorWallet} hWallet
* @param {String} txId
* @param {number | null | undefined} timeout
* @returns {Promise<void>}
*/
export async function waitTxConfirmed(hWallet, txId, timeout) {
// Only return the positive response after the tx has a first block
return new Promise(async (resolve, reject) => {
let timeoutHandler;
if (timeout) {
// Timeout handler
timeoutHandler = setTimeout(async () => {
reject(new Error(`Timeout of ${timeout}ms without confirming the transaction`));
}, timeout);
}

while (await getTxFirstBlock(hWallet, txId) === null) {
await delay(1000);
}

if (timeoutHandler) {
clearTimeout(timeoutHandler);
}

resolve();
});
}

/**
* This method returns the first block of a transaction
*
* @param {HathorWallet} hWallet
* @param {String} txId
* @returns {Promise<String>}
*/
export async function getTxFirstBlock(hWallet, txId) {
const txData = await hWallet.getFullTxById(txId);
return get(txData, 'meta.first_block');
}
277 changes: 277 additions & 0 deletions __tests__/integration/nanocontracts/bet.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import { GenesisWalletHelper } from '../helpers/genesis-wallet.helper';
import {
generateWalletHelper,
stopAllWallets,
waitForTxReceived,
waitUntilNextTimestamp,
waitNextBlock,
waitTxConfirmed
} from '../helpers/wallet.helper';
import { HATHOR_TOKEN_CONFIG } from '../../../src/constants';
import ncApi from '../../../src/api/nano';
import helpersUtils from '../../../src/utils/helpers';
import dateFormatter from '../../../src/utils/date';
import { bufferToHex, hexToBuffer } from '../../../src/utils/buffer';
import Address from '../../../src/models/address';
import P2PKH from '../../../src/models/p2pkh';
import { isEmpty } from 'lodash';
import { getOracleBuffer, getOracleInputData } from '../../../src/nano_contracts/utils';
import NanoContractTransactionParser from '../../../src/nano_contracts/parser';
import Serializer from '../../../src/nano_contracts/serializer';

// We have to skip this test because it needs nano contract support in the full node.
// Until we have this support in the public docker image, the CI won't succeed if this is not skipped
// After skipping it, we must also add `--nc-history-index` as a new parameter for the integration tests full node
// and add the blueprints in the configuration file for the tests privnet
describe.skip('full cycle of bet nano contract', () => {
/** @type HathorWallet */
let hWallet;

beforeAll(async () => {
hWallet = await generateWalletHelper();
const tx = await GenesisWalletHelper.injectFunds(hWallet, await hWallet.getAddressAtIndex(0), 1000);
await waitForTxReceived(hWallet, tx.hash);
});

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

const checkTxValid = async (txId) => {
expect(txId).toBeDefined();
await waitForTxReceived(hWallet, txId);
// We need to wait for the tx to get a first block, so we guarantee it was executed
await waitTxConfirmed(hWallet, txId);
// Now we query the transaction from the full node to double check it's still valid after the nano execution
// and it already has a first block, so it was really executed
const txAfterExecution = await hWallet.getFullTxById(txId);
expect(isEmpty(txAfterExecution.meta.voided_by)).toBe(true);
expect(isEmpty(txAfterExecution.meta.first_block)).not.toBeNull();
}

it('bet deposit', async () => {
const address0 = await hWallet.getAddressAtIndex(0);
const address1 = await hWallet.getAddressAtIndex(1);
const dateLastBet = dateFormatter.dateToTimestamp(new Date()) + 6000;
const network = hWallet.getNetworkObject();
const blueprintId = '3cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e771595';

// Create NC
const oracleData = getOracleBuffer(address1, network);
const tx1 = await hWallet.createAndSendNanoContractTransaction(
'initialize',
address0,
{
blueprintId,
args: [
bufferToHex(oracleData),
HATHOR_TOKEN_CONFIG.uid,
dateLastBet
]
}
);
await checkTxValid(tx1.hash);
const tx1Data = await hWallet.getFullTxById(tx1.hash);

const tx1Parser = new NanoContractTransactionParser(blueprintId, 'initialize', tx1Data.tx.nc_pubkey, tx1Data.tx.nc_args);
tx1Parser.parseAddress();
await tx1Parser.parseArguments();
expect(tx1Parser.address.base58).toBe(address0);
expect(tx1Parser.parsedArgs).toStrictEqual([
{ name: 'oracle_script', type: 'bytes', parsed: oracleData },
{ name: 'token_uid', type: 'bytes', parsed: Buffer.from([HATHOR_TOKEN_CONFIG.uid]) },
{ name: 'date_last_offer', type: 'int', parsed: dateLastBet },
]);

// Bet 100 to address 2
const address2 = await hWallet.getAddressAtIndex(2);
const address2Obj = new Address(address2, { network });
const txBet = await hWallet.createAndSendNanoContractTransaction(
'bet',
address2,
{
ncId: tx1.hash,
args: [
bufferToHex(address2Obj.decode()),
'1x0'
],
actions: [
{
type: 'deposit',
token: HATHOR_TOKEN_CONFIG.uid,
amount: 100
}
],
}
);
await checkTxValid(txBet.hash);
const txBetData = await hWallet.getFullTxById(txBet.hash);

const txBetParser = new NanoContractTransactionParser(blueprintId, 'bet', txBetData.tx.nc_pubkey, txBetData.tx.nc_args);
txBetParser.parseAddress();
await txBetParser.parseArguments();
expect(txBetParser.address.base58).toBe(address2);
expect(txBetParser.parsedArgs).toStrictEqual([
{ name: 'address', type: 'bytes', parsed: address2Obj.decode() },
{ name: 'score', type: 'str', parsed: '1x0' },
]);

// Bet 200 to address 3
const address3 = await hWallet.getAddressAtIndex(3);
const address3Obj = new Address(address3, { network });
const txBet2 = await hWallet.createAndSendNanoContractTransaction(
'bet',
address3,
{
ncId: tx1.hash,
args: [
bufferToHex(address3Obj.decode()),
'2x0'
],
actions: [
{
type: 'deposit',
token: HATHOR_TOKEN_CONFIG.uid,
amount: 200
}
],
}
);
await checkTxValid(txBet2.hash);
const txBet2Data = await hWallet.getFullTxById(txBet2.hash);

const txBet2Parser = new NanoContractTransactionParser(blueprintId, 'bet', txBet2Data.tx.nc_pubkey, txBet2Data.tx.nc_args);
txBet2Parser.parseAddress();
await txBet2Parser.parseArguments();
expect(txBet2Parser.address.base58).toBe(address3);
expect(txBet2Parser.parsedArgs).toStrictEqual([
{ name: 'address', type: 'bytes', parsed: address3Obj.decode() },
{ name: 'score', type: 'str', parsed: '2x0' },
]);

// Get nc history
const txIds = [tx1.hash, txBet.hash, txBet2.hash];
const ncHistory = await ncApi.getNanoContractHistory(tx1.hash);
expect(ncHistory.history.length).toBe(3);
for (const tx of ncHistory.history) {
expect(txIds).toContain(tx.hash);
}

// Get NC state
const ncState = await ncApi.getNanoContractState(
tx1.hash,
[
'token_uid',
'total',
'final_result',
'oracle_script',
'date_last_offer',
`address_details.a'${address2}'`,
`withdrawals.a'${address2}'`,
`address_details.a'${address3}'`,
`withdrawals.a'${address3}'`
]
);
const addressObj1 = new Address(address1, { network });
const outputScriptObj1 = new P2PKH(addressObj1);
const outputScriptBuffer1 = outputScriptObj1.createScript();

expect(ncState.fields.token_uid.value).toBe(HATHOR_TOKEN_CONFIG.uid);
expect(ncState.fields.date_last_offer.value).toBe(dateLastBet);
expect(ncState.fields.oracle_script.value).toBe(bufferToHex(outputScriptBuffer1));
expect(ncState.fields.final_result.value).toBeNull();
expect(ncState.fields.total.value).toBe(300);
expect(ncState.fields[`address_details.a'${address2}'`].value).toHaveProperty('1x0', 100);
expect(ncState.fields[`withdrawals.a'${address2}'`].value).toBeUndefined();
expect(ncState.fields[`address_details.a'${address3}'`].value).toHaveProperty('2x0', 200);
expect(ncState.fields[`withdrawals.a'${address3}'`].value).toBeUndefined();

// Set result to '1x0'
const nanoSerializer = new Serializer();
const result = '1x0';
const resultSerialized = nanoSerializer.serializeFromType(result, 'str');
const inputData = await getOracleInputData(oracleData, resultSerialized, hWallet);
const txSetResult = await hWallet.createAndSendNanoContractTransaction(
'set_result',
address1,
{
ncId: tx1.hash,
args: [
`${bufferToHex(inputData)},${result},str`
],
}
);
await checkTxValid(txSetResult.hash);
txIds.push(txSetResult.hash);

const txSetResultData = await hWallet.getFullTxById(txSetResult.hash);

const txSetResultParser = new NanoContractTransactionParser(blueprintId, 'set_result', txSetResultData.tx.nc_pubkey, txSetResultData.tx.nc_args);
txSetResultParser.parseAddress();
await txSetResultParser.parseArguments();
expect(txSetResultParser.address.base58).toBe(address1);
expect(txSetResultParser.parsedArgs).toStrictEqual([
{ name: 'result', type: 'SignedData[str]', parsed: `${bufferToHex(inputData)},${result},str` },
]);

// Try to withdraw to address 2, success
const txWithdrawal = await hWallet.createAndSendNanoContractTransaction(
'withdraw',
address2,
{
ncId: tx1.hash,
actions: [
{
type: 'withdrawal',
token: HATHOR_TOKEN_CONFIG.uid,
amount: 300,
address: address2
}
],
}
);
await checkTxValid(txWithdrawal.hash);
txIds.push(txWithdrawal.hash);

const txWithdrawalData = await hWallet.getFullTxById(txWithdrawal.hash);

const txWithdrawalParser = new NanoContractTransactionParser(blueprintId, 'set_result', txWithdrawalData.tx.nc_pubkey, txWithdrawalData.tx.nc_args);
txWithdrawalParser.parseAddress();
await txWithdrawalParser.parseArguments();
expect(txWithdrawalParser.address.base58).toBe(address2);
expect(txWithdrawalParser.parsedArgs).toBe(null);

// Get state again
const ncState2 = await ncApi.getNanoContractState(
tx1.hash,
[
'token_uid',
'total',
'final_result',
'oracle_script',
'date_last_offer',
`address_details.a'${address2}'`,
`withdrawals.a'${address2}'`,
`address_details.a'${address3}'`,
`withdrawals.a'${address3}'`
]
);
expect(ncState2.fields.token_uid.value).toBe(HATHOR_TOKEN_CONFIG.uid);
expect(ncState2.fields.date_last_offer.value).toBe(dateLastBet);
expect(ncState2.fields.oracle_script.value).toBe(bufferToHex(outputScriptBuffer1));
expect(ncState2.fields.final_result.value).toBe('1x0');
expect(ncState2.fields.total.value).toBe(300);
expect(ncState2.fields[`address_details.a'${address2}'`].value).toHaveProperty('1x0', 100);
expect(ncState2.fields[`withdrawals.a'${address2}'`].value).toBe(300);
expect(ncState2.fields[`address_details.a'${address3}'`].value).toHaveProperty('2x0', 200);
expect(ncState2.fields[`withdrawals.a'${address3}'`].value).toBeUndefined();

// Get history again
const ncHistory2 = await ncApi.getNanoContractHistory(tx1.hash);
expect(ncHistory2.history.length).toBe(5);
for (const tx of ncHistory2.history) {
expect(txIds).toContain(tx.hash);
}
});
});
Loading

0 comments on commit 54b250c

Please sign in to comment.