accounts-resolver.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import camelCase from "camelcase";
  2. import { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from "@solana/web3.js";
  3. import { Idl, IdlSeed, IdlAccount } from "../idl.js";
  4. import * as utf8 from "../utils/bytes/utf8.js";
  5. import { TOKEN_PROGRAM_ID, ASSOCIATED_PROGRAM_ID } from "../utils/token.js";
  6. import { AllInstructions } from "./namespace/types.js";
  7. import Provider from "../provider.js";
  8. import { AccountNamespace } from "./namespace/account.js";
  9. import { coder } from "../spl/token";
  10. // Populates a given accounts context with PDAs and common missing accounts.
  11. export class AccountsResolver<IDL extends Idl, I extends AllInstructions<IDL>> {
  12. static readonly CONST_ACCOUNTS = {
  13. systemProgram: SystemProgram.programId,
  14. tokenProgram: TOKEN_PROGRAM_ID,
  15. associatedTokenProgram: ASSOCIATED_PROGRAM_ID,
  16. rent: SYSVAR_RENT_PUBKEY,
  17. };
  18. private _accountStore: AccountStore<IDL>;
  19. constructor(
  20. private _args: Array<any>,
  21. private _accounts: { [name: string]: PublicKey },
  22. private _provider: Provider,
  23. private _programId: PublicKey,
  24. private _idlIx: AllInstructions<IDL>,
  25. _accountNamespace: AccountNamespace<IDL>
  26. ) {
  27. this._accountStore = new AccountStore(_provider, _accountNamespace);
  28. }
  29. // Note: We serially resolve PDAs one by one rather than doing them
  30. // in parallel because there can be dependencies between
  31. // addresses. That is, one PDA can be used as a seed in another.
  32. //
  33. // TODO: PDAs need to be resolved in topological order. For now, we
  34. // require the developer to simply list the accounts in the
  35. // correct order. But in future work, we should create the
  36. // dependency graph and resolve automatically.
  37. //
  38. public async resolve() {
  39. for (let k = 0; k < this._idlIx.accounts.length; k += 1) {
  40. // Cast is ok because only a non-nested IdlAccount can have a seeds
  41. // cosntraint.
  42. const accountDesc = this._idlIx.accounts[k] as IdlAccount;
  43. const accountDescName = camelCase(accountDesc.name);
  44. // PDA derived from IDL seeds.
  45. if (
  46. accountDesc.pda &&
  47. accountDesc.pda.seeds.length > 0 &&
  48. !this._accounts[accountDescName]
  49. ) {
  50. await this.autoPopulatePda(accountDesc);
  51. continue;
  52. }
  53. // Signers default to the provider.
  54. if (accountDesc.isSigner && !this._accounts[accountDescName]) {
  55. this._accounts[accountDescName] = this._provider.wallet.publicKey;
  56. continue;
  57. }
  58. // Common accounts are auto populated with magic names by convention.
  59. if (
  60. Reflect.has(AccountsResolver.CONST_ACCOUNTS, accountDescName) &&
  61. !this._accounts[accountDescName]
  62. ) {
  63. this._accounts[accountDescName] =
  64. AccountsResolver.CONST_ACCOUNTS[accountDescName];
  65. }
  66. }
  67. }
  68. private async autoPopulatePda(accountDesc: IdlAccount) {
  69. if (!accountDesc.pda || !accountDesc.pda.seeds)
  70. throw new Error("Must have seeds");
  71. const seeds: Buffer[] = await Promise.all(
  72. accountDesc.pda.seeds.map((seedDesc: IdlSeed) => this.toBuffer(seedDesc))
  73. );
  74. const programId = await this.parseProgramId(accountDesc);
  75. const [pubkey] = await PublicKey.findProgramAddress(seeds, programId);
  76. this._accounts[camelCase(accountDesc.name)] = pubkey;
  77. }
  78. private async parseProgramId(accountDesc: IdlAccount): Promise<PublicKey> {
  79. if (!accountDesc.pda?.programId) {
  80. return this._programId;
  81. }
  82. switch (accountDesc.pda.programId.kind) {
  83. case "const":
  84. return new PublicKey(
  85. this.toBufferConst(accountDesc.pda.programId.value)
  86. );
  87. case "arg":
  88. return this.argValue(accountDesc.pda.programId);
  89. case "account":
  90. return await this.accountValue(accountDesc.pda.programId);
  91. default:
  92. throw new Error(
  93. `Unexpected program seed kind: ${accountDesc.pda.programId.kind}`
  94. );
  95. }
  96. }
  97. private async toBuffer(seedDesc: IdlSeed): Promise<Buffer> {
  98. switch (seedDesc.kind) {
  99. case "const":
  100. return this.toBufferConst(seedDesc);
  101. case "arg":
  102. return await this.toBufferArg(seedDesc);
  103. case "account":
  104. return await this.toBufferAccount(seedDesc);
  105. default:
  106. throw new Error(`Unexpected seed kind: ${seedDesc.kind}`);
  107. }
  108. }
  109. private toBufferConst(seedDesc: IdlSeed): Buffer {
  110. return this.toBufferValue(seedDesc.type, seedDesc.value);
  111. }
  112. private async toBufferArg(seedDesc: IdlSeed): Promise<Buffer> {
  113. const argValue = this.argValue(seedDesc);
  114. return this.toBufferValue(seedDesc.type, argValue);
  115. }
  116. private argValue(seedDesc: IdlSeed): any {
  117. const seedArgName = camelCase(seedDesc.path.split(".")[0]);
  118. const idlArgPosition = this._idlIx.args.findIndex(
  119. (argDesc: any) => argDesc.name === seedArgName
  120. );
  121. if (idlArgPosition === -1) {
  122. throw new Error(`Unable to find argument for seed: ${seedArgName}`);
  123. }
  124. return this._args[idlArgPosition];
  125. }
  126. private async toBufferAccount(seedDesc: IdlSeed): Promise<Buffer> {
  127. const accountValue = await this.accountValue(seedDesc);
  128. return this.toBufferValue(seedDesc.type, accountValue);
  129. }
  130. private async accountValue(seedDesc: IdlSeed): Promise<any> {
  131. const pathComponents = seedDesc.path.split(".");
  132. const fieldName = pathComponents[0];
  133. const fieldPubkey = this._accounts[camelCase(fieldName)];
  134. // The seed is a pubkey of the account.
  135. if (pathComponents.length === 1) {
  136. return fieldPubkey;
  137. }
  138. // The key is account data.
  139. //
  140. // Fetch and deserialize it.
  141. const account = await this._accountStore.fetchAccount(
  142. seedDesc.account,
  143. fieldPubkey
  144. );
  145. // Dereference all fields in the path to get the field value
  146. // used in the seed.
  147. const fieldValue = this.parseAccountValue(account, pathComponents.slice(1));
  148. return fieldValue;
  149. }
  150. private parseAccountValue<T = any>(account: T, path: Array<string>): any {
  151. let accountField: any;
  152. while (path.length > 0) {
  153. accountField = account[camelCase(path[0])];
  154. path = path.slice(1);
  155. }
  156. return accountField;
  157. }
  158. // Converts the given idl valaue into a Buffer. The values here must be
  159. // primitives. E.g. no structs.
  160. //
  161. // TODO: add more types here as needed.
  162. private toBufferValue(type: string | any, value: any): Buffer {
  163. switch (type) {
  164. case "u8":
  165. return Buffer.from([value]);
  166. case "u16":
  167. let b = Buffer.alloc(2);
  168. b.writeUInt16LE(value);
  169. return b;
  170. case "u32":
  171. let buf = Buffer.alloc(4);
  172. buf.writeUInt32LE(value);
  173. return buf;
  174. case "u64":
  175. let bU64 = Buffer.alloc(8);
  176. bU64.writeBigUInt64LE(BigInt(value));
  177. return bU64;
  178. case "string":
  179. return Buffer.from(utf8.encode(value));
  180. case "publicKey":
  181. return value.toBuffer();
  182. default:
  183. if (type.array) {
  184. return Buffer.from(value);
  185. }
  186. throw new Error(`Unexpected seed type: ${type}`);
  187. }
  188. }
  189. }
  190. // TODO: this should be configureable to avoid unnecessary requests.
  191. export class AccountStore<IDL extends Idl> {
  192. private _cache = new Map<string, any>();
  193. // todo: don't use the progrma use the account namespace.
  194. constructor(
  195. private _provider: Provider,
  196. private _accounts: AccountNamespace<IDL>
  197. ) {}
  198. public async fetchAccount<T = any>(
  199. name: string,
  200. publicKey: PublicKey
  201. ): Promise<T> {
  202. const address = publicKey.toString();
  203. if (!this._cache.has(address)) {
  204. if (name === "TokenAccount") {
  205. const accountInfo = await this._provider.connection.getAccountInfo(
  206. publicKey
  207. );
  208. if (accountInfo === null) {
  209. throw new Error(`invalid account info for ${address}`);
  210. }
  211. const data = coder().accounts.decode("Token", accountInfo.data);
  212. this._cache.set(address, data);
  213. } else {
  214. const account = this._accounts[camelCase(name)].fetch(publicKey);
  215. this._cache.set(address, account);
  216. }
  217. }
  218. return this._cache.get(address);
  219. }
  220. }