Skip to content

Commit

Permalink
[transaction] Add ability to get user transaction hash
Browse files Browse the repository at this point in the history
  • Loading branch information
gregnazario committed Apr 5, 2024
1 parent 5e3aa62 commit 9ddfaf0
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 1 deletion.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T
- Add `message` input type verification on `sign` and `verifySignature` functions and convert it into a correct type if needed
- [`Breaking`] Change `fromString` to `fromHexString` on `Hex` class
- Introduce Serializable `SimpleTransaction` and `MultiAgentTransaction` modules
- [`Breaking] Change any generate transaction function to return `SimpleTransaction` or `MultiAgentTransaction` instance
- [`Breaking`] Change any generate transaction function to return `SimpleTransaction` or `MultiAgentTransaction` instance
- Adds `getUserTransactionHash` which can generate a transaction hash after signing, but before submission.t

# 1.11.0 (2024-03-26)

Expand Down
30 changes: 30 additions & 0 deletions src/transactions/transactionBuilder/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,36 @@ export function generateSignedTransaction(args: InputSubmitTransactionData): Uin
);
}

/**
* Hashes the set of values with a SHA-3 256 hash
* @param input array of UTF-8 strings or Uint8array byte arrays
*/
export function hashValues(input: (Uint8Array | string)[]): Uint8Array {
const hash = sha3Hash.create();
for (const item of input) {
hash.update(item);
}
return hash.digest();
}

/**
* The domain separated prefix for hashing transacitons
*/
const TRANSACTION_PREFIX = hashValues(["APTOS::Transaction"]);

/**
* Generates a user transaction hash for the given transaction payload. It must already have an authenticator
* @param args InputSubmitTransactionData
*/
export function generateUserTransactionHash(args: InputSubmitTransactionData): string {
const signedTransaction = generateSignedTransaction(args);

// Transaction signature is defined as, the domain separated prefix based on struct (Transaction)
// Then followed by the type of the transaction for the enum, UserTransaction is 0
// Then followed by BCS encoded bytes of the signed transaction
return new Hex(hashValues([TRANSACTION_PREFIX, new Uint8Array([0]), signedTransaction])).toString();
}

/**
* Derive the raw transaction type - FeePayerRawTransaction or MultiAgentRawTransaction or RawTransaction
*
Expand Down
97 changes: 97 additions & 0 deletions tests/e2e/transaction/transactionBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
generateTransactionPayloadWithABI,
sign,
SignedTransaction,
generateUserTransactionHash,
} from "../../../src";
import { FUND_AMOUNT, longTestTimeout } from "../../unit/helper";
import { getAptosClient } from "../helper";
Expand Down Expand Up @@ -552,6 +553,102 @@ describe("transaction builder", () => {
expect(signedTransaction instanceof SignedTransaction).toBeTruthy();
});
});

describe("generateUserTransactionHash", () => {
test("it generates a single signer signed transaction hash", async () => {
const alice = Account.generate();
await aptos.fundAccount({ accountAddress: alice.accountAddress, amount: FUND_AMOUNT });
const transaction = await aptos.coin.transferCoinTransaction({
sender: alice.accountAddress,
recipient: "0x1",
amount: 1,
});

const senderAuthenticator = sign({ signer: alice, transaction });

// Generate hash
const signedTxnInput = {
transaction,
senderAuthenticator,
};
const transactionHash = generateUserTransactionHash(signedTxnInput);

// Submit transaction
const submitted = await aptos.transaction.submit.simple(signedTxnInput);

expect(submitted.hash).toBe(transactionHash);
});

test("it generates a multi agent signed transaction", async () => {
const alice = Account.generate();
await aptos.fundAccount({ accountAddress: alice.accountAddress, amount: FUND_AMOUNT });
const bob = await Account.generate();
await aptos.fundAccount({ accountAddress: bob.accountAddress, amount: 1 });
const payload = await generateTransactionPayload({
aptosConfig: config,
function: `${contractPublisherAccount.accountAddress}::transfer::transfer`,
functionArguments: [1, bob.accountAddress],
});
const transaction = await buildTransaction({
aptosConfig: config,
sender: alice.accountAddress,
payload,
secondarySignerAddresses: [bob.accountAddress],
});
const senderAuthenticator = sign({ signer: alice, transaction });
const secondaryAuthenticator = sign({
signer: bob,
transaction,
});
// Generate hash
const signedTxnInput = {
transaction,
senderAuthenticator,
additionalSignersAuthenticators: [secondaryAuthenticator],
};
const transactionHash = generateUserTransactionHash(signedTxnInput);

// Submit transaction
const submitted = await aptos.transaction.submit.multiAgent(signedTxnInput);
expect(submitted.hash).toBe(transactionHash);
});

test("it generates a fee payer signed transaction", async () => {
const alice = Account.generate();
const bob = Account.generate();
await aptos.fundAccount({ accountAddress: alice.accountAddress, amount: FUND_AMOUNT });
const transaction = await aptos.transaction.build.simple({
sender: bob.accountAddress,
data: {
function: "0x1::aptos_account::transfer",
functionArguments: ["0x1", 1],
},
withFeePayer: true,
});

// Bob signs without knowing the fee payer
const senderAuthenticator = sign({
signer: bob,
transaction,
});
// Alice signs after putting themselves in as fee payer
transaction.feePayerAddress = alice.accountAddress;
const feePayerAuthenticator = sign({ signer: alice, transaction });

// Generate hash
const signedTxnInput = {
transaction,
senderAuthenticator,
feePayerAuthenticator,
};
const transactionHash = generateUserTransactionHash(signedTxnInput);

// Submit transaction
const submitted = await aptos.transaction.submit.simple(signedTxnInput);

expect(submitted.hash).toBe(transactionHash);
});
});
describe("deriveTransactionType", () => {
test("it derives the transaction type as a RawTransaction", async () => {
const alice = Account.generate();
Expand Down

0 comments on commit 9ddfaf0

Please sign in to comment.