Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 7 additions & 3 deletions packages/wallet-service/src/api/txProposalCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,13 @@ export const create = middy(walletIdProxyHandler(async (walletId, event) => {

// mark utxos with tx-proposal id
const txProposalId = uuidv4();
await markUtxosWithProposalId(mysql, txProposalId, inputUtxos);

await createTxProposal(mysql, txProposalId, walletId, now);
// Nano contract transactions might have empty inputs
if (inputUtxos.length > 0) {
await markUtxosWithProposalId(mysql, txProposalId, inputUtxos);
}

await closeDbConnection(mysql);
await createTxProposal(mysql, txProposalId, walletId, now);

const inputPromises = inputUtxos.map(async (utxo) => {
const addressDetail: AddressInfo = await getWalletAddressDetail(mysql, walletId, utxo.address);
Expand All @@ -136,6 +138,8 @@ export const create = middy(walletIdProxyHandler(async (walletId, event) => {

const retInputs = await Promise.all(inputPromises);

await closeDbConnection(mysql);

return {
statusCode: 201,
body: JSON.stringify({
Expand Down
4 changes: 4 additions & 0 deletions packages/wallet-service/src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,10 @@ export const getUtxos = async (
mysql: ServerlessMysql,
utxosInfo: IWalletInput[],
): Promise<DbTxOutput[]> => {
if (utxosInfo.length <= 0) {
return [];
}

const entries = utxosInfo.map((utxo) => [utxo.txId, utxo.index]);
const results: DbSelectResult = await mysql.query(
`SELECT *
Expand Down
124 changes: 82 additions & 42 deletions packages/wallet-service/tests/txProposal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ beforeEach(async () => {
genesis_block_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe',
genesis_tx1_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe',
genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe',
native_token: { name: 'Hathor', symbol: 'HTR'},
native_token: { name: 'Hathor', symbol: 'HTR' },
};

await addToVersionDataTable(mysql, now, versionData);
Expand Down Expand Up @@ -174,8 +174,8 @@ test('POST /txproposals with utxos that are already used on another txproposal s
new hathorLib.Output(
300n,
p2pkhAddress, {
tokenData: 1,
},
tokenData: 1,
},
),
];
const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)];
Expand Down Expand Up @@ -221,7 +221,7 @@ test('POST /txproposals with too many outputs should fail with ApiError.TOO_MANY
genesis_block_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe',
genesis_tx1_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe',
genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe',
native_token: { name: 'Hathor', symbol: 'HTR'},
native_token: { name: 'Hathor', symbol: 'HTR' },
});
jest.resetModules();

Expand Down Expand Up @@ -453,8 +453,8 @@ test('PUT /txproposals/{proposalId} with an empty body should fail with ApiError
new hathorLib.P2PKH(new hathorLib.Address(ADDRESSES[0], {
network: new hathorLib.Network(process.env.NETWORK),
})).createScript(), {
tokenData: 1,
},
tokenData: 1,
},
),
];
const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)];
Expand Down Expand Up @@ -594,12 +594,12 @@ test('PUT /txproposals/{proposalId} on a proposal which status is not OPEN or SE
new hathorLib.P2PKH(
new hathorLib.Address(
ADDRESSES[0], {
network: new hathorLib.Network(process.env.NETWORK),
},
network: new hathorLib.Network(process.env.NETWORK),
},
),
).createScript(), {
tokenData: 1,
},
tokenData: 1,
},
),
];
const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)];
Expand Down Expand Up @@ -718,11 +718,11 @@ test('PUT /txproposals/{proposalId} on a proposal which is not owned by the user
300n,
new hathorLib.P2PKH(new hathorLib.Address(
ADDRESSES[0], {
network: new hathorLib.Network(process.env.NETWORK),
},
)).createScript(), {
tokenData: 1,
network: new hathorLib.Network(process.env.NETWORK),
},
)).createScript(), {
tokenData: 1,
},
),
];
const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)];
Expand Down Expand Up @@ -869,11 +869,11 @@ test('PUT /txproposals/{proposalId} with an invalid txHex should fail and update
300n,
new hathorLib.P2PKH(new hathorLib.Address(
ADDRESSES[0], {
network: new hathorLib.Network(process.env.NETWORK),
},
)).createScript(), {
tokenData: 1,
network: new hathorLib.Network(process.env.NETWORK),
},
)).createScript(), {
tokenData: 1,
},
),
];
const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)];
Expand Down Expand Up @@ -1013,11 +1013,11 @@ test('PUT /txproposals/{proposalId} should update tx_proposal to SEND_ERROR on f
300n,
new hathorLib.P2PKH(new hathorLib.Address(
ADDRESSES[0], {
network: new hathorLib.Network(process.env.NETWORK),
},
)).createScript(), {
tokenData: 1,
network: new hathorLib.Network(process.env.NETWORK),
},
)).createScript(), {
tokenData: 1,
},
),
];
const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)];
Expand Down Expand Up @@ -1133,11 +1133,11 @@ test('DELETE /txproposals/{proposalId} should delete a tx_proposal and remove th
300n,
new hathorLib.P2PKH(new hathorLib.Address(
ADDRESSES[0], {
network: new hathorLib.Network(process.env.NETWORK),
},
)).createScript(), {
tokenData: 1,
network: new hathorLib.Network(process.env.NETWORK),
},
)).createScript(), {
tokenData: 1,
},
),
];
const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)];
Expand Down Expand Up @@ -1303,11 +1303,11 @@ test('POST /txproposals one output and input on txHex', async () => {
300n,
new hathorLib.P2PKH(new hathorLib.Address(
ADDRESSES[0], {
network: new hathorLib.Network(process.env.NETWORK),
},
)).createScript(), {
tokenData: 1,
network: new hathorLib.Network(process.env.NETWORK),
},
)).createScript(), {
tokenData: 1,
},
),
];
const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)];
Expand Down Expand Up @@ -1406,11 +1406,11 @@ test('POST /txproposals with denied utxos', async () => {
300n,
new hathorLib.P2PKH(new hathorLib.Address(
ADDRESSES[0], {
network: new hathorLib.Network(process.env.NETWORK),
},
)).createScript(), {
tokenData: 1,
network: new hathorLib.Network(process.env.NETWORK),
},
)).createScript(), {
tokenData: 1,
},
),
];
const inputs = [new hathorLib.Input(utxos[0].txId, utxos[0].index)];
Expand Down Expand Up @@ -1475,8 +1475,8 @@ test('POST /txproposals a tx create action on txHex', async () => {
new hathorLib.P2PKH(
new hathorLib.Address(
ADDRESSES[0], {
network: new hathorLib.Network(process.env.NETWORK),
},
network: new hathorLib.Network(process.env.NETWORK),
},
),
).createScript(),
{ tokenData: 0 },
Expand All @@ -1487,8 +1487,8 @@ test('POST /txproposals a tx create action on txHex', async () => {
new hathorLib.P2PKH(
new hathorLib.Address(
ADDRESSES[0], {
network: new hathorLib.Network(process.env.NETWORK),
},
network: new hathorLib.Network(process.env.NETWORK),
},
),
).createScript(),
{ tokenData: 1 | hathorLib.constants.TOKEN_AUTHORITY_MASK }, // eslint-disable-line no-bitwise
Expand All @@ -1499,8 +1499,8 @@ test('POST /txproposals a tx create action on txHex', async () => {
new hathorLib.P2PKH(
new hathorLib.Address(
ADDRESSES[0], {
network: new hathorLib.Network(process.env.NETWORK),
},
network: new hathorLib.Network(process.env.NETWORK),
},
),
).createScript(),
{ tokenData: 1 | hathorLib.constants.TOKEN_AUTHORITY_MASK }, // eslint-disable-line no-bitwise
Expand All @@ -1511,8 +1511,8 @@ test('POST /txproposals a tx create action on txHex', async () => {
new hathorLib.P2PKH(
new hathorLib.Address(
ADDRESSES[0], {
network: new hathorLib.Network(process.env.NETWORK),
},
network: new hathorLib.Network(process.env.NETWORK),
},
),
).createScript(),
{ tokenData: 1 },
Expand Down Expand Up @@ -1851,3 +1851,43 @@ test('checkMissingUtxos', async () => {

expect(checkMissingResult).toHaveLength(1);
});

test('POST /txproposals with empty inputs array should succeed', async () => {
expect.hasAssertions();

await addToWalletTable(mysql, [{
id: 'my-wallet',
xpubkey: 'xpubkey',
authXpubkey: 'auth_xpubkey',
status: 'ready',
maxGap: 5,
createdAt: 10000,
readyAt: 10001,
}]);
await addToAddressTable(mysql, [{
address: ADDRESSES[0],
index: 0,
walletId: 'my-wallet',
transactions: 2,
}]);

// Create a transaction with no inputs and no outputs (e.g., for nano contracts)
const outputs = []; // Empty outputs array
const inputs = []; // Empty inputs array
const transaction = new hathorLib.Transaction(inputs, outputs);

const txHex = transaction.toHex();
const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex }));

const result = await txProposalCreate(event, null, null) as APIGatewayProxyResult;
const returnBody = JSON.parse(result.body as string);

expect(result.statusCode).toBe(201);
expect(returnBody.success).toBe(true);
expect(returnBody.txProposalId).toHaveLength(36);
expect(returnBody.inputs).toHaveLength(0);

// Verify that the tx proposal was created
const txProposal = await getTxProposal(mysql, returnBody.txProposalId);
expect(txProposal).not.toBeNull();
});