@aptos-labs/ts-sdk
Version:
Aptos TypeScript SDK
158 lines • 8.82 kB
JavaScript
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0
import { AccountAddress } from "../../core/index.js";
import { AuthenticationKey } from "../../core/authenticationKey.js";
import { fetchAndCacheAuthKeyForAddress } from "../../internal/account.js";
import { fetchAndCacheEncryptionKey } from "../../internal/encryptionKey.js";
import { ClaimedEntryFunction, DECRYPTION_NONCE_LENGTH, DecryptedPlaintext, PayloadAssociatedData, } from "../instances/encryptedPayload.js";
import { Identifier } from "../instances/identifier.js";
import { ModuleId } from "../instances/moduleId.js";
import { TransactionExecutableEntryFunction, TransactionExtraConfigV1, TransactionInnerPayloadV1, TransactionPayloadEncryptedPayload, TransactionPayloadMultiSig, } from "../instances/transactionPayload.js";
import { convertPayloadToInnerPayload } from "./transactionBuilder.js";
function payloadToExecutable(payload) {
if (payload instanceof TransactionInnerPayloadV1) {
return { executable: payload.executable, extraConfig: payload.extra_config };
}
const inner = convertPayloadToInnerPayload(payload);
return { executable: inner.executable, extraConfig: inner.extra_config };
}
function toClaimedEntryFunction(input) {
if (input instanceof ClaimedEntryFunction) {
return input;
}
return new ClaimedEntryFunction(ModuleId.fromStr(input.module), input.functionName !== undefined ? new Identifier(input.functionName) : undefined);
}
function assertClaimMatchesExecutable(payload, claim) {
const { executable } = payloadToExecutable(payload);
if (!(executable instanceof TransactionExecutableEntryFunction)) {
throw new Error("claimedEntryFunction is only valid when the plaintext executable is an entry function.");
}
const entry = executable.entryFunction;
if (!entry.module_name.address.equals(claim.moduleId.address) ||
entry.module_name.name.identifier !== claim.moduleId.name.identifier) {
throw new Error("claimedEntryFunction.module must match the entry function module.");
}
if (claim.functionName !== undefined && entry.function_name.identifier !== claim.functionName.identifier) {
throw new Error("claimedEntryFunction.functionName must match the entry function name when provided.");
}
}
function payloadHasMultisigAddress(payload) {
if (payload instanceof TransactionPayloadMultiSig) {
return true;
}
if (payload instanceof TransactionInnerPayloadV1) {
const ec = payload.extra_config;
return ec instanceof TransactionExtraConfigV1 && ec.multisigAddress !== undefined;
}
return false;
}
function resolveClaimedEntryFun(args) {
const { payload, feePayerAddress, options } = args;
// Unlike buildSignerAuthKeys, we treat a zero feePayerAddress (deferred gas-station sponsor) the
// same as a real one: a fee payer *will* sign, so include claimed_entry_fun so they can inspect
// the payload without decrypting it. The zero check in buildSignerAuthKeys is different — adding
// a placeholder zero auth key to the cryptographic AAD would corrupt it.
const hasFeePayer = feePayerAddress !== undefined;
if (!hasFeePayer && !payloadHasMultisigAddress(payload)) {
return undefined;
}
if (options.claimedEntryFunction !== undefined) {
const claim = toClaimedEntryFunction(options.claimedEntryFunction);
assertClaimMatchesExecutable(payload, claim);
return claim;
}
const { executable } = payloadToExecutable(payload);
if (executable instanceof TransactionExecutableEntryFunction) {
return ClaimedEntryFunction.fromEntryFunction(executable.entryFunction);
}
return undefined;
}
function resolveAuthKey(input) {
if (input instanceof AuthenticationKey) {
return input;
}
return new AuthenticationKey({ data: input });
}
/**
* Assembles `(address, authenticationKey)` pairs in `TransactionAuthenticator::all_signer_auth_keys` order
* (sender, secondaries, fee payer last). Auth keys not supplied in `options` are fetched from chain via
* `fetchAndCacheAuthKeyForAddress`, which caches per `(network, address)` for ~1 hour.
*/
async function buildSignerAuthKeys(args) {
const { aptosConfig, sender, options, feePayerAddress, secondarySignerAddresses } = args;
const secondaryAddrs = secondarySignerAddresses ?? [];
const secondaryAuthInputs = options.secondarySignerAuthenticationKeys;
if (secondaryAddrs.length === 0 && secondaryAuthInputs !== undefined && secondaryAuthInputs.length > 0) {
throw new Error("options.secondarySignerAuthenticationKeys was set but no secondarySignerAddresses were provided to generateRawTransaction.");
}
if (secondaryAddrs.length > 0 &&
secondaryAuthInputs !== undefined &&
secondaryAuthInputs.length !== secondaryAddrs.length) {
throw new Error("Encrypted multi-agent transactions require options.secondarySignerAuthenticationKeys (when provided) to have one entry per secondarySignerAddresses entry, in the same order. " +
"Leave individual entries undefined to fetch them from chain.");
}
const feePayerAddr = feePayerAddress !== undefined ? AccountAddress.from(feePayerAddress) : undefined;
const hasNonZeroFeePayer = feePayerAddr !== undefined && !feePayerAddr.equals(AccountAddress.ZERO);
if (options.feePayerAuthenticationKey !== undefined && !hasNonZeroFeePayer) {
throw new Error("options.feePayerAuthenticationKey was set but feePayerAddress is missing or the zero address (no on-chain fee payer for AAD).");
}
const resolveFor = async (address, input) => {
if (input !== undefined) {
return resolveAuthKey(input);
}
return fetchAndCacheAuthKeyForAddress({ aptosConfig, accountAddress: address });
};
const secondaryPairsPromise = Promise.all(secondaryAddrs.map(async (addr, i) => {
const address = AccountAddress.from(addr);
const authenticationKey = await resolveFor(address, secondaryAuthInputs?.[i]);
return { address, authenticationKey };
}));
const [senderAuthKey, secondaryPairs, feePayerAuthKey] = await Promise.all([
resolveFor(sender, options.senderAuthenticationKey),
secondaryPairsPromise,
hasNonZeroFeePayer ? resolveFor(feePayerAddr, options.feePayerAuthenticationKey) : Promise.resolve(undefined),
]);
const senderPair = { address: sender, authenticationKey: senderAuthKey };
const additional = [...secondaryPairs];
if (hasNonZeroFeePayer && feePayerAuthKey !== undefined) {
additional.push({ address: feePayerAddr, authenticationKey: feePayerAuthKey });
}
return { sender: senderPair, additional: additional.length > 0 ? additional : undefined };
}
/**
* Encrypts an entry-function/script/inner payload using the node's per-epoch batch encryption key.
* Validates `options.encrypted` requirements first.
*
* @group Implementation
* @category Transactions
*/
export async function buildEncryptedPayload(args) {
const { aptosConfig, sender, payload, options, feePayerAddress, secondarySignerAddresses, replayProtectionNonce } = args;
const senderAddr = AccountAddress.from(sender);
const { sender: senderPair, additional } = await buildSignerAuthKeys({
aptosConfig,
sender: senderAddr,
options,
feePayerAddress,
secondarySignerAddresses,
});
const claimedEntryFunction = resolveClaimedEntryFun({ payload, feePayerAddress, options });
const encryption = await fetchAndCacheEncryptionKey({ aptosConfig });
if (!encryption) {
throw new Error("Encrypted transactions requested but the node does not provide an encryption key. " +
"Ensure the node supports encrypted transaction submission.");
}
const { key: encryptionKey, epoch: encryptionEpoch } = encryption;
const { executable, extraConfig: baseExtraConfig } = payloadToExecutable(payload);
let extraConfig = baseExtraConfig;
if (replayProtectionNonce !== undefined && extraConfig instanceof TransactionExtraConfigV1) {
extraConfig = new TransactionExtraConfigV1(extraConfig.multisigAddress, replayProtectionNonce);
}
const decryptionNonce = new Uint8Array(DECRYPTION_NONCE_LENGTH);
crypto.getRandomValues(decryptionNonce);
const decryptedPayload = new DecryptedPlaintext(executable, decryptionNonce);
const associatedData = new PayloadAssociatedData(senderAddr, [senderPair, ...(additional ?? [])]);
const ciphertext = encryptionKey.encrypt(decryptedPayload, associatedData);
return new TransactionPayloadEncryptedPayload(ciphertext, extraConfig, decryptedPayload.hash(), encryptionEpoch, claimedEntryFunction);
}
//# sourceMappingURL=encryptPayload.js.map