instructionsPage.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import {
  2. accountValueNode,
  3. argumentValueNode,
  4. constantDiscriminatorNode,
  5. constantPdaSeedNodeFromString,
  6. constantValueNodeFromBytes,
  7. fieldDiscriminatorNode,
  8. instructionAccountNode,
  9. instructionArgumentNode,
  10. instructionNode,
  11. numberTypeNode,
  12. numberValueNode,
  13. pdaNode,
  14. pdaSeedValueNode,
  15. pdaValueNode,
  16. programNode,
  17. publicKeyTypeNode,
  18. resolverValueNode,
  19. variablePdaSeedNode,
  20. } from '@kinobi-so/nodes';
  21. import { visit } from '@kinobi-so/visitors-core';
  22. import { test } from 'vitest';
  23. import { getRenderMapVisitor } from '../src';
  24. import { codeContains, codeDoesNotContain, renderMapContains, renderMapContainsImports } from './_setup';
  25. test('it renders instruction accounts that can either be signer or non-signer', async () => {
  26. // Given the following instruction with a signer or non-signer account.
  27. const node = programNode({
  28. instructions: [
  29. instructionNode({
  30. accounts: [instructionAccountNode({ isSigner: 'either', isWritable: false, name: 'myAccount' })],
  31. name: 'myInstruction',
  32. }),
  33. ],
  34. name: 'myProgram',
  35. publicKey: '1111',
  36. });
  37. // When we render it.
  38. const renderMap = visit(node, getRenderMapVisitor());
  39. // Then we expect the input to be rendered as either a signer or non-signer.
  40. await renderMapContains(renderMap, 'instructions/myInstruction.ts', [
  41. 'myAccount: Address<TAccountMyAccount> | TransactionSigner<TAccountMyAccount>;',
  42. ]);
  43. });
  44. test('it renders extra arguments that default on each other', async () => {
  45. // Given the following instruction with two extra arguments
  46. // such that one defaults to the other.
  47. const node = programNode({
  48. instructions: [
  49. instructionNode({
  50. extraArguments: [
  51. instructionArgumentNode({
  52. defaultValue: argumentValueNode('bar'),
  53. name: 'foo',
  54. type: numberTypeNode('u64'),
  55. }),
  56. instructionArgumentNode({
  57. name: 'bar',
  58. type: numberTypeNode('u64'),
  59. }),
  60. ],
  61. name: 'create',
  62. }),
  63. ],
  64. name: 'myProgram',
  65. publicKey: '1111',
  66. });
  67. // When we render it.
  68. const renderMap = visit(node, getRenderMapVisitor());
  69. // Then we expect the following code to be rendered.
  70. await renderMapContains(renderMap, 'instructions/create.ts', [
  71. 'const args = { ...input }',
  72. 'if (!args.foo) { args.foo = expectSome(args.bar); }',
  73. ]);
  74. });
  75. test('it renders the args variable on the async function only if the extra argument has an async default value', async () => {
  76. // Given the following instruction with an async resolver and an extra argument.
  77. const node = programNode({
  78. instructions: [
  79. instructionNode({
  80. extraArguments: [
  81. instructionArgumentNode({
  82. defaultValue: resolverValueNode('myAsyncResolver'),
  83. name: 'foo',
  84. type: numberTypeNode('u64'),
  85. }),
  86. ],
  87. name: 'create',
  88. }),
  89. ],
  90. name: 'myProgram',
  91. publicKey: '1111',
  92. });
  93. // When we render it.
  94. const renderMap = visit(node, getRenderMapVisitor({ asyncResolvers: ['myAsyncResolver'] }));
  95. // And split the async and sync functions.
  96. const [asyncFunction, syncFunction] = renderMap
  97. .get('instructions/create.ts')
  98. .split(/export\s+function\s+getCreateInstruction/);
  99. // Then we expect only the async function to contain the args variable.
  100. await codeContains(asyncFunction, ['// Original args.', 'const args = { ...input }']);
  101. await codeDoesNotContain(syncFunction, ['// Original args.', 'const args = { ...input }']);
  102. });
  103. test('it only renders the args variable on the async function if the extra argument is used in an async default value', async () => {
  104. // Given the following instruction with an async resolver depending on
  105. // an extra argument such that the instruction has no data arguments.
  106. const node = programNode({
  107. instructions: [
  108. instructionNode({
  109. accounts: [
  110. instructionAccountNode({
  111. defaultValue: resolverValueNode('myAsyncResolver', { dependsOn: [argumentValueNode('bar')] }),
  112. isSigner: false,
  113. isWritable: false,
  114. name: 'foo',
  115. }),
  116. ],
  117. extraArguments: [
  118. instructionArgumentNode({
  119. name: 'bar',
  120. type: numberTypeNode('u64'),
  121. }),
  122. ],
  123. name: 'create',
  124. }),
  125. ],
  126. name: 'myProgram',
  127. publicKey: '1111',
  128. });
  129. // When we render it.
  130. const renderMap = visit(node, getRenderMapVisitor({ asyncResolvers: ['myAsyncResolver'] }));
  131. // And split the async and sync functions.
  132. const [asyncFunction, syncFunction] = renderMap
  133. .get('instructions/create.ts')
  134. .split(/export\s+function\s+getCreateInstruction/);
  135. // Then we expect only the async function to contain the args variable.
  136. await codeContains(asyncFunction, ['// Original args.', 'const args = { ...input }']);
  137. await codeDoesNotContain(syncFunction, ['// Original args.', 'const args = { ...input }']);
  138. });
  139. test('it renders instruction accounts with linked PDAs as default value', async () => {
  140. // Given the following program with a PDA node and an instruction account using it as default value.
  141. const node = programNode({
  142. instructions: [
  143. instructionNode({
  144. accounts: [
  145. instructionAccountNode({ isSigner: true, isWritable: false, name: 'authority' }),
  146. instructionAccountNode({
  147. defaultValue: pdaValueNode('counter', [
  148. pdaSeedValueNode('authority', accountValueNode('authority')),
  149. ]),
  150. isSigner: false,
  151. isWritable: false,
  152. name: 'counter',
  153. }),
  154. ],
  155. name: 'increment',
  156. }),
  157. ],
  158. name: 'counter',
  159. pdas: [
  160. pdaNode({
  161. name: 'counter',
  162. seeds: [
  163. constantPdaSeedNodeFromString('utf8', 'counter'),
  164. variablePdaSeedNode('authority', publicKeyTypeNode()),
  165. ],
  166. }),
  167. ],
  168. publicKey: '1111',
  169. });
  170. // When we render it.
  171. const renderMap = visit(node, getRenderMapVisitor());
  172. // Then we expect the following default value to be rendered.
  173. await renderMapContains(renderMap, 'instructions/increment.ts', [
  174. 'if (!accounts.counter.value) { ' +
  175. 'accounts.counter.value = await findCounterPda( { authority: expectAddress ( accounts.authority.value ) } ); ' +
  176. '}',
  177. ]);
  178. renderMapContainsImports(renderMap, 'instructions/increment.ts', { '../pdas': ['findCounterPda'] });
  179. });
  180. test('it renders instruction accounts with inlined PDAs as default value', async () => {
  181. // Given the following instruction with an inlined PDA default value.
  182. const node = programNode({
  183. instructions: [
  184. instructionNode({
  185. accounts: [
  186. instructionAccountNode({ isSigner: true, isWritable: false, name: 'authority' }),
  187. instructionAccountNode({
  188. defaultValue: pdaValueNode(
  189. pdaNode({
  190. name: 'counter',
  191. seeds: [
  192. constantPdaSeedNodeFromString('utf8', 'counter'),
  193. variablePdaSeedNode('authority', publicKeyTypeNode()),
  194. ],
  195. }),
  196. [pdaSeedValueNode('authority', accountValueNode('authority'))],
  197. ),
  198. isSigner: false,
  199. isWritable: false,
  200. name: 'counter',
  201. }),
  202. ],
  203. name: 'increment',
  204. }),
  205. ],
  206. name: 'counter',
  207. publicKey: '1111',
  208. });
  209. // When we render it.
  210. const renderMap = visit(node, getRenderMapVisitor());
  211. // Then we expect the following default value to be rendered.
  212. await renderMapContains(renderMap, 'instructions/increment.ts', [
  213. 'if (!accounts.counter.value) { ' +
  214. 'accounts.counter.value = await getProgramDerivedAddress( { ' +
  215. ' programAddress, ' +
  216. ' seeds: [ ' +
  217. " getUtf8Encoder().encode('counter'), " +
  218. ' getAddressEncoder().encode(expectAddress(accounts.authority.value)) ' +
  219. ' ] ' +
  220. '} ); ' +
  221. '}',
  222. ]);
  223. renderMapContainsImports(renderMap, 'instructions/increment.ts', {
  224. '@solana/web3.js': ['getProgramDerivedAddress'],
  225. });
  226. });
  227. test('it renders instruction accounts with inlined PDAs from another program as default value', async () => {
  228. // Given the following instruction with an inlined PDA default value from another program.
  229. const node = programNode({
  230. instructions: [
  231. instructionNode({
  232. accounts: [
  233. instructionAccountNode({ isSigner: true, isWritable: false, name: 'authority' }),
  234. instructionAccountNode({
  235. defaultValue: pdaValueNode(
  236. pdaNode({
  237. name: 'counter',
  238. programId: '2222',
  239. seeds: [
  240. constantPdaSeedNodeFromString('utf8', 'counter'),
  241. variablePdaSeedNode('authority', publicKeyTypeNode()),
  242. ],
  243. }),
  244. [pdaSeedValueNode('authority', accountValueNode('authority'))],
  245. ),
  246. isSigner: false,
  247. isWritable: false,
  248. name: 'counter',
  249. }),
  250. ],
  251. name: 'increment',
  252. }),
  253. ],
  254. name: 'counter',
  255. publicKey: '1111',
  256. });
  257. // When we render it.
  258. const renderMap = visit(node, getRenderMapVisitor());
  259. // Then we expect the following default value to be rendered.
  260. await renderMapContains(renderMap, 'instructions/increment.ts', [
  261. 'if (!accounts.counter.value) { ' +
  262. 'accounts.counter.value = await getProgramDerivedAddress( { ' +
  263. " programAddress: '2222' as Address<'2222'>, " +
  264. ' seeds: [ ' +
  265. " getUtf8Encoder().encode('counter'), " +
  266. ' getAddressEncoder().encode(expectAddress(accounts.authority.value)) ' +
  267. ' ] ' +
  268. '} ); ' +
  269. '}',
  270. ]);
  271. renderMapContainsImports(renderMap, 'instructions/increment.ts', {
  272. '@solana/web3.js': ['Address', 'getProgramDerivedAddress'],
  273. });
  274. });
  275. test('it renders constants for instruction field discriminators', async () => {
  276. // Given the following instruction with a field discriminator.
  277. const node = programNode({
  278. instructions: [
  279. instructionNode({
  280. arguments: [
  281. instructionArgumentNode({
  282. defaultValue: numberValueNode(42),
  283. defaultValueStrategy: 'omitted',
  284. name: 'myDiscriminator',
  285. type: numberTypeNode('u64'),
  286. }),
  287. ],
  288. discriminators: [fieldDiscriminatorNode('myDiscriminator')],
  289. name: 'myInstruction',
  290. }),
  291. ],
  292. name: 'myProgram',
  293. publicKey: '1111',
  294. });
  295. // When we render it.
  296. const renderMap = visit(node, getRenderMapVisitor());
  297. // Then we expect the following constant and function to be rendered
  298. // And we expect the field default value to use that constant.
  299. await renderMapContains(renderMap, 'instructions/myInstruction.ts', [
  300. 'export const MY_INSTRUCTION_MY_DISCRIMINATOR = 42;',
  301. 'export function getMyInstructionMyDiscriminatorBytes() { return getU64Encoder().encode(MY_INSTRUCTION_MY_DISCRIMINATOR); }',
  302. '(value) => ({ ...value, myDiscriminator: MY_INSTRUCTION_MY_DISCRIMINATOR })',
  303. ]);
  304. });
  305. test('it renders constants for instruction constant discriminators', async () => {
  306. // Given the following instruction with two constant discriminators.
  307. const node = programNode({
  308. instructions: [
  309. instructionNode({
  310. discriminators: [
  311. constantDiscriminatorNode(constantValueNodeFromBytes('base16', '1111')),
  312. constantDiscriminatorNode(constantValueNodeFromBytes('base16', '2222'), 2),
  313. ],
  314. name: 'myInstruction',
  315. }),
  316. ],
  317. name: 'myProgram',
  318. publicKey: '1111',
  319. });
  320. // When we render it.
  321. const renderMap = visit(node, getRenderMapVisitor());
  322. // Then we expect the following constants and functions to be rendered.
  323. await renderMapContains(renderMap, 'instructions/myInstruction.ts', [
  324. 'export const MY_INSTRUCTION_DISCRIMINATOR = new Uint8Array([ 17, 17 ]);',
  325. 'export function getMyInstructionDiscriminatorBytes() { return getBytesEncoder().encode(MY_INSTRUCTION_DISCRIMINATOR); }',
  326. 'export const MY_INSTRUCTION_DISCRIMINATOR2 = new Uint8Array([ 34, 34 ]);',
  327. 'export function getMyInstructionDiscriminator2Bytes() { return getBytesEncoder().encode(MY_INSTRUCTION_DISCRIMINATOR2); }',
  328. ]);
  329. });
  330. test('it can override the import of a resolver value node', async () => {
  331. // Given the following node with a resolver value node.
  332. const node = programNode({
  333. instructions: [
  334. instructionNode({
  335. accounts: [
  336. instructionAccountNode({
  337. defaultValue: resolverValueNode('myResolver'),
  338. isSigner: false,
  339. isWritable: false,
  340. name: 'myAccount',
  341. }),
  342. ],
  343. name: 'myInstruction',
  344. }),
  345. ],
  346. name: 'myProgram',
  347. pdas: [pdaNode({ name: 'counter', seeds: [] })],
  348. publicKey: '1111',
  349. });
  350. // When we render it using a custom import.
  351. const renderMap = visit(
  352. node,
  353. getRenderMapVisitor({
  354. linkOverrides: {
  355. resolvers: { myResolver: 'someModule' },
  356. },
  357. }),
  358. );
  359. // Then we expect the resolver to be exported.
  360. await renderMapContains(renderMap, 'instructions/myInstruction.ts', ['myResolver(resolverScope)']);
  361. // And its import path to be overridden.
  362. await renderMapContainsImports(renderMap, 'instructions/myInstruction.ts', {
  363. someModule: ['myResolver'],
  364. });
  365. });