import { Address, getAddressComparator } from '@solana/addresses'; import { SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_CANNOT_PAY_FEES, SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE, SolanaError, } from '@solana/errors'; import { AccountMeta, AccountRole, Instruction, isSignerRole, isWritableRole, mergeRoles, ReadonlyAccount, ReadonlySignerAccount, WritableAccount, WritableSignerAccount, } from '@solana/instructions'; import { Brand } from '@solana/nominal-types'; export const enum AddressMapEntryType { FEE_PAYER, STATIC, } type AddressMap = { [address: string]: FeePayerAccountEntry | StaticAccountEntry; }; type FeePayerAccountEntry = Omit & { [TYPE]: AddressMapEntryType.FEE_PAYER; }; export type OrderedAccounts = Brand; type StaticAccountEntry = Omit< ReadonlyAccount | ReadonlySignerAccount | WritableAccount | WritableSignerAccount, 'address' > & { [TYPE]: AddressMapEntryType.STATIC }; function upsert( addressMap: AddressMap, address: Address, update: (entry: FeePayerAccountEntry | Record | StaticAccountEntry) => AddressMap[Address], ) { addressMap[address] = update(addressMap[address] ?? { role: AccountRole.READONLY }); } const TYPE = Symbol('AddressMapTypeProperty'); export const ADDRESS_MAP_TYPE_PROPERTY: typeof TYPE = TYPE; export function getAddressMapFromInstructions(feePayer: Address, instructions: readonly Instruction[]): AddressMap { const addressMap: AddressMap = { [feePayer]: { [TYPE]: AddressMapEntryType.FEE_PAYER, role: AccountRole.WRITABLE_SIGNER }, }; const addressesOfInvokedPrograms = new Set
(); for (const instruction of instructions) { upsert(addressMap, instruction.programAddress, entry => { addressesOfInvokedPrograms.add(instruction.programAddress); if (TYPE in entry) { if (isWritableRole(entry.role)) { switch (entry[TYPE]) { case AddressMapEntryType.FEE_PAYER: throw new SolanaError(SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_CANNOT_PAY_FEES, { programAddress: instruction.programAddress, }); default: throw new SolanaError(SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE, { programAddress: instruction.programAddress, }); } } if (entry[TYPE] === AddressMapEntryType.STATIC) { return entry; } } return { [TYPE]: AddressMapEntryType.STATIC, role: AccountRole.READONLY }; }); if (!instruction.accounts) { continue; } for (const account of instruction.accounts) { upsert(addressMap, account.address, entry => { const { // eslint-disable-next-line @typescript-eslint/no-unused-vars address: _, ...accountMeta } = account; if (TYPE in entry) { switch (entry[TYPE]) { case AddressMapEntryType.FEE_PAYER: // The fee payer already has the highest rank -- it is by definition // writable-signer. Return it, no matter how `account` is configured return entry; case AddressMapEntryType.STATIC: { const nextRole = mergeRoles(entry.role, accountMeta.role); if ( // Check to see if this address represents a program that is invoked // in this transaction. addressesOfInvokedPrograms.has(account.address) ) { if (isWritableRole(accountMeta.role)) { throw new SolanaError( SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE, { programAddress: account.address, }, ); } if (entry.role !== nextRole) { return { ...entry, role: nextRole, } as StaticAccountEntry; } else { return entry; } } else { if (entry.role !== nextRole) { // The account's role ranks higher than the current entry's. return { ...entry, role: nextRole, } as StaticAccountEntry; } else { return entry; } } } } } return { ...accountMeta, [TYPE]: AddressMapEntryType.STATIC, }; }); } } return addressMap; } export function getOrderedAccountsFromAddressMap(addressMap: AddressMap): OrderedAccounts { let addressComparator: ReturnType; const orderedAccounts: AccountMeta[] = Object.entries(addressMap) .sort(([leftAddress, leftEntry], [rightAddress, rightEntry]) => { // STEP 1: Rapid precedence check. Fee payer, then static addresses if (leftEntry[TYPE] !== rightEntry[TYPE]) { if (leftEntry[TYPE] === AddressMapEntryType.FEE_PAYER) { return -1; } else if (rightEntry[TYPE] === AddressMapEntryType.FEE_PAYER) { return 1; } else if (leftEntry[TYPE] === AddressMapEntryType.STATIC) { return -1; } else if (rightEntry[TYPE] === AddressMapEntryType.STATIC) { return 1; } } // STEP 2: Sort by signer-writability. const leftIsSigner = isSignerRole(leftEntry.role); if (leftIsSigner !== isSignerRole(rightEntry.role)) { return leftIsSigner ? -1 : 1; } const leftIsWritable = isWritableRole(leftEntry.role); if (leftIsWritable !== isWritableRole(rightEntry.role)) { return leftIsWritable ? -1 : 1; } // STEP 3: Sort by address. addressComparator ||= getAddressComparator(); return addressComparator(leftAddress, rightAddress); }) .map(([address, addressMeta]) => ({ address: address as Address, ...addressMeta, })); return orderedAccounts as unknown as OrderedAccounts; }