coder.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. import camelCase from "camelcase";
  2. import { Layout } from "buffer-layout";
  3. import { sha256 } from "crypto-hash";
  4. import * as borsh from "@project-serum/borsh";
  5. import {
  6. Idl,
  7. IdlField,
  8. IdlTypeDef,
  9. IdlEnumVariant,
  10. IdlType,
  11. IdlStateMethod,
  12. } from "./idl";
  13. import { IdlError } from "./error";
  14. /**
  15. * Number of bytes of the account discriminator.
  16. */
  17. export const ACCOUNT_DISCRIMINATOR_SIZE = 8;
  18. /**
  19. * Coder provides a facade for encoding and decoding all IDL related objects.
  20. */
  21. export default class Coder {
  22. /**
  23. * Instruction coder.
  24. */
  25. readonly instruction: InstructionCoder;
  26. /**
  27. * Account coder.
  28. */
  29. readonly accounts: AccountsCoder;
  30. /**
  31. * Types coder.
  32. */
  33. readonly types: TypesCoder;
  34. /**
  35. * Coder for state structs.
  36. */
  37. readonly state: StateCoder;
  38. constructor(idl: Idl) {
  39. this.instruction = new InstructionCoder(idl);
  40. this.accounts = new AccountsCoder(idl);
  41. this.types = new TypesCoder(idl);
  42. if (idl.state) {
  43. this.state = new StateCoder(idl);
  44. }
  45. }
  46. }
  47. /**
  48. * Encodes and decodes program instructions.
  49. */
  50. class InstructionCoder<T = any> {
  51. /**
  52. * Instruction enum layout.
  53. */
  54. private ixLayout: Layout;
  55. public constructor(idl: Idl) {
  56. this.ixLayout = InstructionCoder.parseIxLayout(idl);
  57. }
  58. public encode(ix: T): Buffer {
  59. const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
  60. const len = this.ixLayout.encode(ix, buffer);
  61. return buffer.slice(0, len);
  62. }
  63. public decode(ix: Buffer): T {
  64. return this.ixLayout.decode(ix);
  65. }
  66. private static parseIxLayout(idl: Idl): Layout {
  67. let stateMethods = idl.state ? idl.state.methods : [];
  68. let ixLayouts = stateMethods
  69. .map((m: IdlStateMethod) => {
  70. let fieldLayouts = m.args.map((arg: IdlField) =>
  71. IdlCoder.fieldLayout(arg, idl.types)
  72. );
  73. const name = camelCase(m.name);
  74. return borsh.struct(fieldLayouts, name);
  75. })
  76. .concat(
  77. idl.instructions.map((ix) => {
  78. let fieldLayouts = ix.args.map((arg: IdlField) =>
  79. IdlCoder.fieldLayout(arg, idl.types)
  80. );
  81. const name = camelCase(ix.name);
  82. return borsh.struct(fieldLayouts, name);
  83. })
  84. );
  85. return borsh.rustEnum(ixLayouts);
  86. }
  87. }
  88. /**
  89. * Encodes and decodes account objects.
  90. */
  91. class AccountsCoder {
  92. /**
  93. * Maps account type identifier to a layout.
  94. */
  95. private accountLayouts: Map<string, Layout>;
  96. public constructor(idl: Idl) {
  97. if (idl.accounts === undefined) {
  98. this.accountLayouts = new Map();
  99. return;
  100. }
  101. const layouts: [string, Layout][] = idl.accounts.map((acc) => {
  102. return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
  103. });
  104. this.accountLayouts = new Map(layouts);
  105. }
  106. public async encode<T = any>(
  107. accountName: string,
  108. account: T
  109. ): Promise<Buffer> {
  110. const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
  111. const layout = this.accountLayouts.get(accountName);
  112. const len = layout.encode(account, buffer);
  113. let accountData = buffer.slice(0, len);
  114. let discriminator = await accountDiscriminator(accountName);
  115. return Buffer.concat([discriminator, accountData]);
  116. }
  117. public decode<T = any>(accountName: string, ix: Buffer): T {
  118. // Chop off the discriminator before decoding.
  119. const data = ix.slice(8);
  120. const layout = this.accountLayouts.get(accountName);
  121. return layout.decode(data);
  122. }
  123. }
  124. /**
  125. * Encodes and decodes user defined types.
  126. */
  127. class TypesCoder {
  128. /**
  129. * Maps account type identifier to a layout.
  130. */
  131. private layouts: Map<string, Layout>;
  132. public constructor(idl: Idl) {
  133. if (idl.types === undefined) {
  134. this.layouts = new Map();
  135. return;
  136. }
  137. const layouts = idl.types.map((acc) => {
  138. return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
  139. });
  140. // @ts-ignore
  141. this.layouts = new Map(layouts);
  142. }
  143. public encode<T = any>(accountName: string, account: T): Buffer {
  144. const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
  145. const layout = this.layouts.get(accountName);
  146. const len = layout.encode(account, buffer);
  147. return buffer.slice(0, len);
  148. }
  149. public decode<T = any>(accountName: string, ix: Buffer): T {
  150. const layout = this.layouts.get(accountName);
  151. return layout.decode(ix);
  152. }
  153. }
  154. class StateCoder {
  155. private layout: Layout;
  156. public constructor(idl: Idl) {
  157. if (idl.state === undefined) {
  158. throw new Error("Idl state not defined.");
  159. }
  160. this.layout = IdlCoder.typeDefLayout(idl.state.struct, idl.types);
  161. }
  162. public async encode<T = any>(name: string, account: T): Promise<Buffer> {
  163. const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
  164. const len = this.layout.encode(account, buffer);
  165. const disc = await stateDiscriminator(name);
  166. const accData = buffer.slice(0, len);
  167. return Buffer.concat([disc, accData]);
  168. }
  169. public decode<T = any>(ix: Buffer): T {
  170. // Chop off discriminator.
  171. const data = ix.slice(8);
  172. return this.layout.decode(data);
  173. }
  174. }
  175. class IdlCoder {
  176. public static fieldLayout(field: IdlField, types?: IdlTypeDef[]): Layout {
  177. const fieldName =
  178. field.name !== undefined ? camelCase(field.name) : undefined;
  179. switch (field.type) {
  180. case "bool": {
  181. return borsh.bool(fieldName);
  182. }
  183. case "u8": {
  184. return borsh.u8(fieldName);
  185. }
  186. case "u32": {
  187. return borsh.u32(fieldName);
  188. }
  189. case "u64": {
  190. return borsh.u64(fieldName);
  191. }
  192. case "i64": {
  193. return borsh.i64(fieldName);
  194. }
  195. case "bytes": {
  196. return borsh.vecU8(fieldName);
  197. }
  198. case "string": {
  199. return borsh.str(fieldName);
  200. }
  201. case "publicKey": {
  202. return borsh.publicKey(fieldName);
  203. }
  204. // TODO: all the other types that need to be exported by the borsh package.
  205. default: {
  206. // @ts-ignore
  207. if (field.type.vec) {
  208. return borsh.vec(
  209. IdlCoder.fieldLayout(
  210. {
  211. name: undefined,
  212. // @ts-ignore
  213. type: field.type.vec,
  214. },
  215. types
  216. ),
  217. fieldName
  218. );
  219. // @ts-ignore
  220. } else if (field.type.option) {
  221. return borsh.option(
  222. IdlCoder.fieldLayout(
  223. {
  224. name: undefined,
  225. // @ts-ignore
  226. type: field.type.option,
  227. },
  228. types
  229. ),
  230. fieldName
  231. );
  232. // @ts-ignore
  233. } else if (field.type.defined) {
  234. // User defined type.
  235. if (types === undefined) {
  236. throw new IdlError("User defined types not provided");
  237. }
  238. // @ts-ignore
  239. const filtered = types.filter((t) => t.name === field.type.defined);
  240. if (filtered.length !== 1) {
  241. throw new IdlError(`Type not found: ${JSON.stringify(field)}`);
  242. }
  243. return IdlCoder.typeDefLayout(filtered[0], types, fieldName);
  244. } else {
  245. throw new Error(`Not yet implemented: ${field}`);
  246. }
  247. }
  248. }
  249. }
  250. public static typeDefLayout(
  251. typeDef: IdlTypeDef,
  252. types: IdlTypeDef[],
  253. name?: string
  254. ): Layout {
  255. if (typeDef.type.kind === "struct") {
  256. const fieldLayouts = typeDef.type.fields.map((field) => {
  257. const x = IdlCoder.fieldLayout(field, types);
  258. return x;
  259. });
  260. return borsh.struct(fieldLayouts, name);
  261. } else if (typeDef.type.kind === "enum") {
  262. let variants = typeDef.type.variants.map((variant: IdlEnumVariant) => {
  263. const name = camelCase(variant.name);
  264. if (variant.fields === undefined) {
  265. return borsh.struct([], name);
  266. }
  267. // @ts-ignore
  268. const fieldLayouts = variant.fields.map((f: IdlField | IdlType) => {
  269. // @ts-ignore
  270. if (f.name === undefined) {
  271. throw new Error("Tuple enum variants not yet implemented.");
  272. }
  273. // @ts-ignore
  274. return IdlCoder.fieldLayout(f, types);
  275. });
  276. return borsh.struct(fieldLayouts, name);
  277. });
  278. if (name !== undefined) {
  279. // Buffer-layout lib requires the name to be null (on construction)
  280. // when used as a field.
  281. return borsh.rustEnum(variants).replicate(name);
  282. }
  283. return borsh.rustEnum(variants, name);
  284. } else {
  285. throw new Error(`Unknown type kint: ${typeDef}`);
  286. }
  287. }
  288. }
  289. // Calculates unique 8 byte discriminator prepended to all anchor accounts.
  290. export async function accountDiscriminator(name: string): Promise<Buffer> {
  291. return Buffer.from(
  292. (
  293. await sha256(`account:${name}`, {
  294. outputFormat: "buffer",
  295. })
  296. ).slice(0, 8)
  297. );
  298. }
  299. // Calculates unique 8 byte discriminator prepended to all anchor state accounts.
  300. export async function stateDiscriminator(name: string): Promise<Buffer> {
  301. return Buffer.from(
  302. (
  303. await sha256(`account:${name}`, {
  304. outputFormat: "buffer",
  305. })
  306. ).slice(0, 8)
  307. );
  308. }
  309. // Returns the size of the type in bytes. For variable length types, just return
  310. // 1. Users should override this value in such cases.
  311. export function typeSize(idl: Idl, ty: IdlType): number {
  312. switch (ty) {
  313. case "bool":
  314. return 1;
  315. case "u8":
  316. return 1;
  317. case "i8":
  318. return 1;
  319. case "u16":
  320. return 2;
  321. case "u32":
  322. return 4;
  323. case "u64":
  324. return 8;
  325. case "i64":
  326. return 8;
  327. case "bytes":
  328. return 1;
  329. case "string":
  330. return 1;
  331. case "publicKey":
  332. return 32;
  333. default:
  334. // @ts-ignore
  335. if (ty.vec !== undefined) {
  336. return 1;
  337. }
  338. // @ts-ignore
  339. if (ty.option !== undefined) {
  340. // @ts-ignore
  341. return 1 + typeSize(ty.option);
  342. }
  343. // @ts-ignore
  344. if (ty.defined !== undefined) {
  345. // @ts-ignore
  346. const filtered = idl.types.filter((t) => t.name === ty.defined);
  347. if (filtered.length !== 1) {
  348. throw new IdlError(`Type not found: ${JSON.stringify(ty)}`);
  349. }
  350. let typeDef = filtered[0];
  351. return accountSize(idl, typeDef);
  352. }
  353. throw new Error(`Invalid type ${JSON.stringify(ty)}`);
  354. }
  355. }
  356. export function accountSize(
  357. idl: Idl,
  358. idlAccount: IdlTypeDef
  359. ): number | undefined {
  360. if (idlAccount.type.kind === "enum") {
  361. let variantSizes = idlAccount.type.variants.map(
  362. (variant: IdlEnumVariant) => {
  363. if (variant.fields === undefined) {
  364. return 0;
  365. }
  366. // @ts-ignore
  367. return (
  368. variant.fields
  369. // @ts-ignore
  370. .map((f: IdlField | IdlType) => {
  371. // @ts-ignore
  372. if (f.name === undefined) {
  373. throw new Error("Tuple enum variants not yet implemented.");
  374. }
  375. // @ts-ignore
  376. return typeSize(idl, f.type);
  377. })
  378. .reduce((a: number, b: number) => a + b)
  379. );
  380. }
  381. );
  382. return Math.max(...variantSizes) + 1;
  383. }
  384. if (idlAccount.type.fields === undefined) {
  385. return 0;
  386. }
  387. return idlAccount.type.fields
  388. .map((f) => typeSize(idl, f.type))
  389. .reduce((a, b) => a + b);
  390. }