@honeycomb-protocol/solita
Version:
Generates SDK API from solana contract IDL.
393 lines (376 loc) • 14.7 kB
JavaScript
"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