import { AccountInfo, AccountSigner, AccountTransactionSignature, MakeRequired } from '../../index.js';
import { assert, assertIn, assertInteger, assertObject, countSignatures, isDefined } from '../../util.js';
import { AccountTransactionV0, AccountTransactionV1, Payload, Transaction } from '../index.js';
import { preFinalized } from './finalized.js';
import { Header, HeaderJSON, headerFromJSON, headerToJSON } from './shared.js';
export type Signable
= SignableV0
| SignableV1
;
export type SignableV0
= {
version: 0;
/**
* The transaction input header of the v0 _signable_ transaction stage, i.e. with everything required to finalize the
* transaction.
*/
header: Required>;
/**
* The transaction payload, defining the transaction type and type specific data.
*/
payload: P;
/**
* The map of signatures for the credentials associated with the account.
*/
signature: AccountTransactionV0.Signature;
};
export type SignableV1 = {
version: 1;
/**
* The transaction input header of the v1 _signable_ transaction stage, i.e. with everything required to finalize the
* transaction.
*/
header: MakeRequired;
/**
* The transaction payload, defining the transaction type and type specific data.
*/
payload: P;
/**
* The signatures for both `sender` and `sponsor`.
*/
signatures: AccountTransactionV1.Signatures;
};
type V0JSON = {
version: 0;
header: HeaderJSON;
payload: Payload.JSON;
signature: AccountTransactionSignature;
};
type V1JSON = {
version: 1;
header: HeaderJSON;
payload: Payload.JSON;
signatures: AccountTransactionV1.Signatures;
};
export type SignableJSON = V0JSON | V1JSON;
function validateSignatureAmount(
signature: AccountTransactionSignature,
numAllowed: bigint,
role: 'sender' | 'sponsor' = 'sender'
): void {
const sigs = countSignatures(signature);
if (sigs > numAllowed)
throw new Error(
`Too many ${role} signatures added to the transaction. Counted ${sigs}, but transaction specifies ${numAllowed} allowed number of signatures.`
);
}
export function isSignable(transaction: Transaction.Type): transaction is Signable {
const hasVersion =
'version' in transaction && typeof transaction.version === 'number' && [0, 1].includes(transaction.version);
const hasSigs = 'signature' in transaction || 'signatures' in transaction;
return hasVersion && hasSigs;
}
/**
* Adds a pre-computed signature to a _signable_ transaction.
*
* @template P - the payload type
* @template T - the transaction type
*
* @param transaction - the transaction to add a signature to
* @param signature - the sender signature on the transaction to add
*
* @returns the signed transaction with the signature attached
* @throws Error if the number of signatures exceeds the allowed number specified in the transaction header
*/
export function addSignature(
transaction: Signable
,
signature: AccountTransactionSignature
): Signable
{
switch (transaction.version) {
case 0: {
const signed: SignableV0
= {
...transaction,
signature,
};
return mergeSignaturesInto(transaction, signed);
}
case 1: {
const signed: SignableV1
= {
...transaction,
signatures: { sender: signature },
};
return mergeSignaturesInto(transaction, signed);
}
}
}
/**
* Signs a _signable_ transaction using the provided account signer.
*
* @template P - the payload type
* @template T - the transaction type
*
* @param transaction - the signable transaction to sign
* @param signer - the account signer to use for signing
*
* @returns a promise that resolves to the signed transaction
* @throws Error if the number of signatures exceeds the allowed number specified in the transaction header
*/
export async function sign
(
transaction: Signable
,
signer: AccountSigner
): Promise> {
let signature: AccountTransactionSignature;
switch (transaction.version) {
case 0: {
signature = await AccountTransactionV0.createSignature(preFinalized(transaction), signer);
break;
}
case 1: {
signature = await AccountTransactionV1.createSignature(preFinalized(transaction), signer);
break;
}
}
return addSignature(transaction, signature);
}
/**
* Adds a pre-computed sponsor signature to a _signable_ transaction.
*
* @template P - the payload type
* @template T - the transaction type
*
* @param transaction - the transaction to add a sponsor signature to
* @param signature - the sponsor signature on the transaction to add
*
* @returns the signed transaction with the sponsor signature attached
* @throws Error if the number of signatures exceeds the allowed number specified in the transaction header
*/
export function addSponsorSignature(
transaction: SignableV1
,
signature: AccountTransactionSignature
): Signable
{
const signed: SignableV1
= {
...transaction,
signatures: { sponsor: signature, sender: {} },
};
return mergeSignaturesInto(transaction, signed);
}
/**
* Signs a _signable_ transaction as a sponsor using the provided account signer.
*
* @template P - the payload type
* @template T - the transaction type
*
* @param transaction - the signable transaction to sign
* @param signer - the account signer to use for signing
*
* @returns a promise that resolves to the signed transaction
* @throws Error if the number of signatures exceeds the allowed number specified in the transaction header
*/
export async function sponsor
(
transaction: SignableV1
,
signer: AccountSigner
): Promise> {
let signature = await AccountTransactionV1.createSignature(preFinalized(transaction), signer);
return addSponsorSignature(transaction, signature);
}
function mergeSignature(a: AccountTransactionSignature, b: AccountTransactionSignature): AccountTransactionSignature;
function mergeSignature(
a: AccountTransactionSignature | undefined,
b: AccountTransactionSignature | undefined
): AccountTransactionSignature | undefined;
function mergeSignature(
a: AccountTransactionSignature | undefined,
b: AccountTransactionSignature | undefined
): AccountTransactionSignature | undefined {
if (a === undefined) return b;
if (b === undefined) return a;
const signature: AccountTransactionSignature = {};
// First, we copy all the signatures from `a`.
for (const credIndex in a) {
signature[credIndex] = { ...a[credIndex] };
}
for (const credIndex in b) {
if (signature[credIndex] === undefined) {
// If signatures don't exist for this credential index, we copy everything and move on.
signature[credIndex] = { ...b[credIndex] };
continue;
}
// Otherwise, check all key indices of the credential signature
for (const keyIndex in b[credIndex]) {
const sig = signature[credIndex][keyIndex];
if (sig !== undefined)
throw new Error(`Duplicate signature found for credential index ${credIndex} at key index ${keyIndex}`);
// Copy the signature found, as it does not already exist
signature[credIndex][keyIndex] = b[credIndex][keyIndex];
}
}
return signature;
}
/**
* Verify an account signature on a transaction.
*
* @param transaction the transaction to verify the signature for.
* @param signature the signature on the transaction, from a specific account.
* @param accountInfo the address and credentials of the account.
*
* @returns whether the signature is valid.
*/
export async function verifySignature(
transaction: Signable,
signature: AccountTransactionSignature,
accountInfo: Pick
): Promise {
switch (transaction.version) {
case 0:
return AccountTransactionV0.verifySignature(preFinalized(transaction), signature, accountInfo);
case 1:
return AccountTransactionV1.verifySignature(preFinalized(transaction), signature, accountInfo);
}
}
/**
* Merges signatures from _signable_ transaction `other` into _signable_ transaction `target`.
* Used for multi-signature scenarios where multiple parties sign the same transaction.
*
* @template P - the payload type
* @template T - the signed transaction type
*
* @param target - the signed transaction to merge signatures into
* @param other - the signed transaction from which the signatures are merged into `target`
*
* @returns `target` with the signatures from `other` added into it.
* @throws Error if duplicate signatures are found for the same credential and key index
* @throws Error if the number of signatures exceeds the allowed number specified in the transaction header
*/
export function mergeSignaturesInto = Signable
>(
target: T,
other: T
): T {
if (target.version !== other.version) throw new Error('"a" is incompatible with "b"');
switch (target.version) {
case 0: {
const signature: AccountTransactionSignature = mergeSignature(
target.signature,
(other as SignableV0).signature
);
validateSignatureAmount(signature, target.header.numSignatures);
target.signature = signature;
return target;
}
case 1: {
const bv1 = other as SignableV1;
const sender = mergeSignature(target.signatures.sender, bv1.signatures.sender);
const sponsor = mergeSignature(target.signatures.sponsor, bv1.signatures.sponsor);
validateSignatureAmount(sender, target.header.numSignatures);
validateSignatureAmount(sponsor ?? {}, target.header.sponsor?.numSignatures ?? 0n);
target.signatures = { sender, sponsor };
return target;
}
}
}
export function signableToJSON(transaction: Signable): SignableJSON {
const json = {
header: headerToJSON(transaction.header),
payload: Payload.toJSON(transaction.payload),
};
if (transaction.version === 0) {
return { version: 0, signature: transaction.signature, ...json };
}
return { version: 1, signatures: transaction.signatures, ...json };
}
function signableHeaderFromJSON(json: unknown): Signable['header'] {
const { sender, nonce, expiry, numSignatures, ...header } = headerFromJSON(json);
assert(isDefined(sender));
assert(isDefined(nonce));
assert(isDefined(expiry));
assert(isDefined(numSignatures));
return { ...header, sender, nonce, expiry, numSignatures };
}
export function signableFromJSON(json: unknown): Signable {
assertIn(json, 'header', 'payload', 'version');
assertInteger(json.version);
const header = signableHeaderFromJSON(json.header);
const payload = Payload.fromJSON(json.payload);
if (Number(json.version) === 0) {
assertIn(json, 'signature');
assertObject(json.signature);
return { version: 0, header, payload, signature: json.signature as AccountTransactionV0.Signature };
}
if (Number(json.version) === 1) {
assertIn(json, 'signatures');
assertObject(json.signatures);
return { version: 1, header, payload, signatures: json.signatures as AccountTransactionV1.Signatures };
}
throw new Error('Failed to parse "Signable" from value');
}