UNPKG

@honeycomb-protocol/solita

Version:

Generates SDK API from solana contract IDL.

393 lines (376 loc) 14.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.renderAccount = void 0; const assert_1 = require("assert"); const render_enums_1 = require("./render-enums"); const serdes_1 = require("./serdes"); const type_mapper_1 = require("./type-mapper"); const types_1 = require("./types"); const utils_1 = require("./utils"); function colonSeparatedTypedField(field, prefix = '') { return `${prefix}${field.name}: ${field.tsType}`; } class AccountRenderer { constructor(account, fullFileDir, hasImplicitDiscriminator, resolveFieldType, programId, typeMapper, serializers) { this.account = account; this.fullFileDir = fullFileDir; this.hasImplicitDiscriminator = hasImplicitDiscriminator; this.resolveFieldType = resolveFieldType; this.programId = programId; this.typeMapper = typeMapper; this.serializers = serializers; this.upperCamelAccountName = account.name .charAt(0) .toUpperCase() .concat(account.name.slice(1)); this.camelAccountName = account.name .charAt(0) .toLowerCase() .concat(account.name.slice(1)); this.accountDataClassName = this.upperCamelAccountName; this.accountDataArgsTypeName = `${this.accountDataClassName}Args`; this.beetName = `${this.camelAccountName}Beet`; this.accountDiscriminatorName = `${this.camelAccountName}Discriminator`; this.serializerSnippets = this.serializers.snippetsFor(this.account.name, this.fullFileDir, this.beetName); this.paddingField = this.getPaddingField(); this.programIdPubkey = `new ${types_1.SOLANA_WEB3_EXPORT_NAME}.PublicKey('${this.programId}')`; } getPaddingField() { const paddingField = this.account.type.fields.filter((f) => (0, types_1.hasPaddingAttr)(f)); if (paddingField.length === 0) return; assert_1.strict.equal(paddingField.length, 1, 'only one field of an account can be padding'); const field = paddingField[0]; const ty = (0, types_1.asIdlTypeArray)(field.type); const [inner, size] = ty.array; assert_1.strict.equal(inner, 'u8', 'padding field must be u8[]'); return { name: field.name, size }; } serdeProcess() { return this.typeMapper.mapSerdeFields(this.account.type.fields); } // ----------------- // Rendered Fields // ----------------- getTypedFields() { return this.account.type.fields.map((f) => { const tsType = this.typeMapper.map(f.type, f.name); return { name: f.name, tsType, isPadding: (0, types_1.hasPaddingAttr)(f) }; }); } getPrettyFields() { return this.account.type.fields .filter((f) => !(0, types_1.hasPaddingAttr)(f)) .map((f) => { if (f.type === 'publicKey') { return `${f.name}: this.${f.name}.toBase58()`; } if (f.type === 'u64' || f.type === 'u128' || f.type === 'u256' || f.type === 'u512' || f.type === 'i64' || f.type === 'i128' || f.type === 'i256' || f.type === 'i512') { return `${f.name}: (() => { const x = <{ toNumber: () => number }>this.${f.name} if (typeof x.toNumber === 'function') { try { return x.toNumber() } catch (_) { return x } } return x })()`; } if ((0, types_1.isIdlTypeDefined)(f.type)) { const resolved = this.resolveFieldType(f.type.defined.name); if (resolved != null && (0, types_1.isIdlTypeScalarEnum)(resolved)) { const tsType = this.typeMapper.map(f.type, f.name); const variant = `${tsType}[this.${f.name}`; return `${f.name}: '${f.type.defined.name}.' + ${variant}]`; } if (resolved != null && (0, types_1.isIdlTypeDataEnum)(resolved)) { // TODO(thlorenz): Improve rendering of data enums to include other fields return `${f.name}: this.${f.name}.__kind`; } } return `${f.name}: this.${f.name}`; }); } // ----------------- // Imports // ----------------- renderImports() { const imports = this.typeMapper.importsUsed(this.fullFileDir.toString(), new Set([types_1.SOLANA_WEB3_PACKAGE, types_1.BEET_PACKAGE, types_1.BEET_SOLANA_PACKAGE])); return imports.join('\n'); } // ----------------- // Account Args // ----------------- renderAccountDataArgsType(fields) { const renderedFields = fields .filter((f) => !f.isPadding) .map((f) => colonSeparatedTypedField(f)) .join('\n '); return `/** * Arguments used to create {@link ${this.accountDataClassName}} * @category Accounts * @category generated */ export type ${this.accountDataArgsTypeName} = { ${renderedFields} }`; } renderByteSizeMethods() { if (this.typeMapper.usedFixableSerde) { const byteSizeValue = this.hasImplicitDiscriminator ? `{ accountDiscriminator: ${this.accountDiscriminatorName}, ...instance, }` : `instance`; return ` /** * Returns the byteSize of a {@link Buffer} holding the serialized data of * {@link ${this.accountDataClassName}} for the provided args. * * @param args need to be provided since the byte size for this account * depends on them */ static byteSize(args: ${this.accountDataArgsTypeName}) { const instance = ${this.accountDataClassName}.fromArgs(args) return ${this.beetName}.toFixedFromValue(${byteSizeValue}).byteSize } /** * Fetches the minimum balance needed to exempt an account holding * {@link ${this.accountDataClassName}} data from rent * * @param args need to be provided since the byte size for this account * depends on them * @param connection used to retrieve the rent exemption information */ static async getMinimumBalanceForRentExemption( args: ${this.accountDataArgsTypeName}, connection: web3.Connection, commitment?: web3.Commitment ): Promise<number> { return connection.getMinimumBalanceForRentExemption( ${this.accountDataClassName}.byteSize(args), commitment ) } `.trim(); } else { return ` /** * Returns the byteSize of a {@link Buffer} holding the serialized data of * {@link ${this.accountDataClassName}} */ static get byteSize() { return ${this.beetName}.byteSize; } /** * Fetches the minimum balance needed to exempt an account holding * {@link ${this.accountDataClassName}} data from rent * * @param connection used to retrieve the rent exemption information */ static async getMinimumBalanceForRentExemption( connection: web3.Connection, commitment?: web3.Commitment, ): Promise<number> { return connection.getMinimumBalanceForRentExemption( ${this.accountDataClassName}.byteSize, commitment, ); } /** * Determines if the provided {@link Buffer} has the correct byte size to * hold {@link ${this.accountDataClassName}} data. */ static hasCorrectByteSize(buf: Buffer, offset = 0) { return buf.byteLength - offset === ${this.accountDataClassName}.byteSize; } `.trim(); } } // ----------------- // AccountData class // ----------------- renderAccountDiscriminatorVar() { if (!this.hasImplicitDiscriminator) return ''; const accountDisc = JSON.stringify(Array.from((0, utils_1.accountDiscriminator)(this.account.name))); return `export const ${this.accountDiscriminatorName} = ${accountDisc}`; } renderSerializeValue() { const serializeValues = []; if (this.hasImplicitDiscriminator) { serializeValues.push(`accountDiscriminator: ${this.accountDiscriminatorName}`); } if (this.paddingField != null) { serializeValues.push(`padding: Array(${this.paddingField.size}).fill(0)`); } return serializeValues.length > 0 ? `{ ${serializeValues.join(',\n ')}, ...this }` : 'this'; } renderAccountDataClass(fields) { const constructorArgs = fields .filter((f) => !f.isPadding) .map((f) => colonSeparatedTypedField(f, 'readonly ')) .join(',\n '); const constructorParams = fields .filter((f) => !f.isPadding) .map((f) => `args.${f.name}`) .join(',\n '); const prettyFields = this.getPrettyFields().join(',\n '); const byteSizeMethods = this.renderByteSizeMethods(); const accountDiscriminatorVar = this.renderAccountDiscriminatorVar(); const serializeValue = this.renderSerializeValue(); return ` ${accountDiscriminatorVar}; /** * Holds the data for the {@link ${this.upperCamelAccountName}} Account and provides de/serialization * functionality for that data * * @category Accounts * @category generated */ export class ${this.accountDataClassName} implements ${this.accountDataArgsTypeName} { private constructor( ${constructorArgs} ) {} /** * Creates a {@link ${this.accountDataClassName}} instance from the provided args. */ static fromArgs(args: ${this.accountDataArgsTypeName}) { return new ${this.accountDataClassName}( ${constructorParams} ); } /** * Deserializes the {@link ${this.accountDataClassName}} from the data of the provided {@link web3.AccountInfo}. * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it. */ static fromAccountInfo( accountInfo: web3.AccountInfo<Buffer>, offset = 0 ): [ ${this.accountDataClassName}, number ] { return ${this.accountDataClassName}.deserialize(accountInfo.data, offset) } /** * Retrieves the account info from the provided address and deserializes * the {@link ${this.accountDataClassName}} from its data. * * @throws Error if no account info is found at the address or if deserialization fails */ static async fromAccountAddress( connection: web3.Connection, address: web3.PublicKey, commitmentOrConfig?: web3.Commitment | web3.GetAccountInfoConfig, ): Promise<${this.accountDataClassName}> { const accountInfo = await connection.getAccountInfo(address, commitmentOrConfig); if (accountInfo == null) { throw new Error(\`Unable to find ${this.accountDataClassName} account at \${address}\`); } return ${this.accountDataClassName}.fromAccountInfo(accountInfo, 0)[0]; } /** * Provides a {@link ${types_1.SOLANA_WEB3_EXPORT_NAME}.Connection.getProgramAccounts} config builder, * to fetch accounts matching filters that can be specified via that builder. * * @param programId - the program that owns the accounts we are filtering */ static gpaBuilder(programId: web3.PublicKey = ${this.programIdPubkey}) { return ${types_1.BEET_SOLANA_EXPORT_NAME}.GpaBuilder.fromStruct(programId, ${this.beetName}) } /** * Deserializes the {@link ${this.accountDataClassName}} from the provided data Buffer. * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it. */ static deserialize( buf: Buffer, offset = 0 ): [ ${this.accountDataClassName}, number ]{ return ${this.serializerSnippets.deserialize}(buf, offset); } /** * Serializes the {@link ${this.accountDataClassName}} into a Buffer. * @returns a tuple of the created Buffer and the offset up to which the buffer was written to store it. */ serialize(): [ Buffer, number ] { return ${this.serializerSnippets.serialize}(${serializeValue}) } ${byteSizeMethods} /** * Returns a readable version of {@link ${this.accountDataClassName}} properties * and can be used to convert to JSON and/or logging */ pretty() { return { ${prettyFields} }; } }`.trim(); } // ----------------- // Struct // ----------------- renderBeet(fields) { let discriminatorName; let discriminatorField; let discriminatorType; if (this.hasImplicitDiscriminator) { discriminatorName = 'accountDiscriminator'; discriminatorField = this.typeMapper.mapSerdeField((0, utils_1.anchorDiscriminatorField)('accountDiscriminator')); discriminatorType = (0, utils_1.anchorDiscriminatorType)(this.typeMapper, `account ${this.account.name} discriminant type`); } const struct = (0, serdes_1.renderDataStruct)({ fields, structVarName: this.beetName, className: this.accountDataClassName, argsTypename: this.accountDataArgsTypeName, discriminatorName, discriminatorField, discriminatorType, paddingField: this.paddingField, isFixable: this.typeMapper.usedFixableSerde, }); return ` /** * @category Accounts * @category generated */ ${struct}`.trim(); } render() { this.typeMapper.clearUsages(); const typedFields = this.getTypedFields(); const beetFields = this.serdeProcess(); const enums = (0, render_enums_1.renderScalarEnums)(this.typeMapper.scalarEnumsUsed).join('\n'); const imports = this.renderImports(); const accountDataArgsType = this.renderAccountDataArgsType(typedFields); const accountDataClass = this.renderAccountDataClass(typedFields); const beetDecl = this.renderBeet(beetFields); return `${imports} ${this.serializerSnippets.importSnippet} ${enums} ${accountDataArgsType} ${accountDataClass} ${beetDecl} ${this.serializerSnippets.resolveFunctionsSnippet}`; } } function renderAccount(account, fullFileDir, accountFilesByType, customFilesByType, externalPackagesByType, typeAliases, serializers, forceFixable, programId, resolveFieldType, hasImplicitDiscriminator) { const typeMapper = new type_mapper_1.TypeMapper(accountFilesByType, customFilesByType, externalPackagesByType, typeAliases, forceFixable); const renderer = new AccountRenderer(account, fullFileDir, hasImplicitDiscriminator, resolveFieldType, programId, typeMapper, serializers); return renderer.render(); } exports.renderAccount = renderAccount; //# sourceMappingURL=render-account.js.map