instructionsPage.test.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  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 '@codama/nodes';
  21. import { visit } from '@codama/visitors-core';
  22. import { test } from 'vitest';
  23. import { getRenderMapVisitor } from '../src';
  24. import {
  25. codeContains,
  26. codeDoesNotContain,
  27. renderMapContains,
  28. renderMapContainsImports,
  29. renderMapDoesNotContain,
  30. } from './_setup';
  31. test('it renders instruction accounts that can either be signer or non-signer', async () => {
  32. // Given the following instruction with a signer or non-signer account.
  33. const node = programNode({
  34. instructions: [
  35. instructionNode({
  36. accounts: [instructionAccountNode({ isSigner: 'either', isWritable: false, name: 'myAccount' })],
  37. name: 'myInstruction',
  38. }),
  39. ],
  40. name: 'myProgram',
  41. publicKey: '1111',
  42. });
  43. // When we render it.
  44. const renderMap = visit(node, getRenderMapVisitor());
  45. // Then we expect the input to be rendered as either a signer or non-signer.
  46. await renderMapContains(renderMap, 'instructions/myInstruction.ts', [
  47. 'myAccount: Address<TAccountMyAccount> | TransactionSigner<TAccountMyAccount>;',
  48. ]);
  49. });
  50. test('it renders extra arguments that default on each other', async () => {
  51. // Given the following instruction with two extra arguments
  52. // such that one defaults to the other.
  53. const node = programNode({
  54. instructions: [
  55. instructionNode({
  56. extraArguments: [
  57. instructionArgumentNode({
  58. defaultValue: argumentValueNode('bar'),
  59. name: 'foo',
  60. type: numberTypeNode('u64'),
  61. }),
  62. instructionArgumentNode({
  63. name: 'bar',
  64. type: numberTypeNode('u64'),
  65. }),
  66. ],
  67. name: 'create',
  68. }),
  69. ],
  70. name: 'myProgram',
  71. publicKey: '1111',
  72. });
  73. // When we render it.
  74. const renderMap = visit(node, getRenderMapVisitor());
  75. // Then we expect the following code to be rendered.
  76. await renderMapContains(renderMap, 'instructions/create.ts', [
  77. 'const args = { ...input }',
  78. 'if (!args.foo) { args.foo = expectSome(args.bar); }',
  79. ]);
  80. });
  81. test('it renders the args variable on the async function only if the extra argument has an async default value', async () => {
  82. // Given the following instruction with an async resolver and an extra argument.
  83. const node = programNode({
  84. instructions: [
  85. instructionNode({
  86. extraArguments: [
  87. instructionArgumentNode({
  88. defaultValue: resolverValueNode('myAsyncResolver'),
  89. name: 'foo',
  90. type: numberTypeNode('u64'),
  91. }),
  92. ],
  93. name: 'create',
  94. }),
  95. ],
  96. name: 'myProgram',
  97. publicKey: '1111',
  98. });
  99. // When we render it.
  100. const renderMap = visit(node, getRenderMapVisitor({ asyncResolvers: ['myAsyncResolver'] }));
  101. // And split the async and sync functions.
  102. const [asyncFunction, syncFunction] = renderMap
  103. .get('instructions/create.ts')
  104. .split(/export\s+function\s+getCreateInstruction/);
  105. // Then we expect only the async function to contain the args variable.
  106. await codeContains(asyncFunction, ['// Original args.', 'const args = { ...input }']);
  107. await codeDoesNotContain(syncFunction, ['// Original args.', 'const args = { ...input }']);
  108. });
  109. test('it only renders the args variable on the async function if the extra argument is used in an async default value', async () => {
  110. // Given the following instruction with an async resolver depending on
  111. // an extra argument such that the instruction has no data arguments.
  112. const node = programNode({
  113. instructions: [
  114. instructionNode({
  115. accounts: [
  116. instructionAccountNode({
  117. defaultValue: resolverValueNode('myAsyncResolver', { dependsOn: [argumentValueNode('bar')] }),
  118. isSigner: false,
  119. isWritable: false,
  120. name: 'foo',
  121. }),
  122. ],
  123. extraArguments: [
  124. instructionArgumentNode({
  125. name: 'bar',
  126. type: numberTypeNode('u64'),
  127. }),
  128. ],
  129. name: 'create',
  130. }),
  131. ],
  132. name: 'myProgram',
  133. publicKey: '1111',
  134. });
  135. // When we render it.
  136. const renderMap = visit(node, getRenderMapVisitor({ asyncResolvers: ['myAsyncResolver'] }));
  137. // And split the async and sync functions.
  138. const [asyncFunction, syncFunction] = renderMap
  139. .get('instructions/create.ts')
  140. .split(/export\s+function\s+getCreateInstruction/);
  141. // Then we expect only the async function to contain the args variable.
  142. await codeContains(asyncFunction, ['// Original args.', 'const args = { ...input }']);
  143. await codeDoesNotContain(syncFunction, ['// Original args.', 'const args = { ...input }']);
  144. });
  145. test('it renders instruction accounts with linked PDAs as default value', async () => {
  146. // Given the following program with a PDA node and an instruction account using it as default value.
  147. const node = programNode({
  148. instructions: [
  149. instructionNode({
  150. accounts: [
  151. instructionAccountNode({ isSigner: true, isWritable: false, name: 'authority' }),
  152. instructionAccountNode({
  153. defaultValue: pdaValueNode('counter', [
  154. pdaSeedValueNode('authority', accountValueNode('authority')),
  155. ]),
  156. isSigner: false,
  157. isWritable: false,
  158. name: 'counter',
  159. }),
  160. ],
  161. name: 'increment',
  162. }),
  163. ],
  164. name: 'counter',
  165. pdas: [
  166. pdaNode({
  167. name: 'counter',
  168. seeds: [
  169. constantPdaSeedNodeFromString('utf8', 'counter'),
  170. variablePdaSeedNode('authority', publicKeyTypeNode()),
  171. ],
  172. }),
  173. ],
  174. publicKey: '1111',
  175. });
  176. // When we render it.
  177. const renderMap = visit(node, getRenderMapVisitor());
  178. // Then we expect the following default value to be rendered.
  179. await renderMapContains(renderMap, 'instructions/increment.ts', [
  180. 'if (!accounts.counter.value) { ' +
  181. 'accounts.counter.value = await findCounterPda( { authority: expectAddress ( accounts.authority.value ) } ); ' +
  182. '}',
  183. ]);
  184. await renderMapContainsImports(renderMap, 'instructions/increment.ts', { '../pdas': ['findCounterPda'] });
  185. });
  186. test('it renders instruction accounts with inlined PDAs as default value', async () => {
  187. // Given the following instruction with an inlined PDA default value.
  188. const node = programNode({
  189. instructions: [
  190. instructionNode({
  191. accounts: [
  192. instructionAccountNode({ isSigner: true, isWritable: false, name: 'authority' }),
  193. instructionAccountNode({
  194. defaultValue: pdaValueNode(
  195. pdaNode({
  196. name: 'counter',
  197. seeds: [
  198. constantPdaSeedNodeFromString('utf8', 'counter'),
  199. variablePdaSeedNode('authority', publicKeyTypeNode()),
  200. ],
  201. }),
  202. [pdaSeedValueNode('authority', accountValueNode('authority'))],
  203. ),
  204. isSigner: false,
  205. isWritable: false,
  206. name: 'counter',
  207. }),
  208. ],
  209. name: 'increment',
  210. }),
  211. ],
  212. name: 'counter',
  213. publicKey: '1111',
  214. });
  215. // When we render it.
  216. const renderMap = visit(node, getRenderMapVisitor());
  217. // Then we expect the following default value to be rendered.
  218. await renderMapContains(renderMap, 'instructions/increment.ts', [
  219. 'if (!accounts.counter.value) { ' +
  220. 'accounts.counter.value = await getProgramDerivedAddress( { ' +
  221. ' programAddress, ' +
  222. ' seeds: [ ' +
  223. " getUtf8Encoder().encode('counter'), " +
  224. ' getAddressEncoder().encode(expectAddress(accounts.authority.value)) ' +
  225. ' ] ' +
  226. '} ); ' +
  227. '}',
  228. ]);
  229. await renderMapContainsImports(renderMap, 'instructions/increment.ts', {
  230. '@solana/web3.js': ['getProgramDerivedAddress'],
  231. });
  232. });
  233. test('it renders instruction accounts with inlined PDAs from another program as default value', async () => {
  234. // Given the following instruction with an inlined PDA default value from another program.
  235. const node = programNode({
  236. instructions: [
  237. instructionNode({
  238. accounts: [
  239. instructionAccountNode({ isSigner: true, isWritable: false, name: 'authority' }),
  240. instructionAccountNode({
  241. defaultValue: pdaValueNode(
  242. pdaNode({
  243. name: 'counter',
  244. programId: '2222',
  245. seeds: [
  246. constantPdaSeedNodeFromString('utf8', 'counter'),
  247. variablePdaSeedNode('authority', publicKeyTypeNode()),
  248. ],
  249. }),
  250. [pdaSeedValueNode('authority', accountValueNode('authority'))],
  251. ),
  252. isSigner: false,
  253. isWritable: false,
  254. name: 'counter',
  255. }),
  256. ],
  257. name: 'increment',
  258. }),
  259. ],
  260. name: 'counter',
  261. publicKey: '1111',
  262. });
  263. // When we render it.
  264. const renderMap = visit(node, getRenderMapVisitor());
  265. // Then we expect the following default value to be rendered.
  266. await renderMapContains(renderMap, 'instructions/increment.ts', [
  267. 'if (!accounts.counter.value) { ' +
  268. 'accounts.counter.value = await getProgramDerivedAddress( { ' +
  269. " programAddress: '2222' as Address<'2222'>, " +
  270. ' seeds: [ ' +
  271. " getUtf8Encoder().encode('counter'), " +
  272. ' getAddressEncoder().encode(expectAddress(accounts.authority.value)) ' +
  273. ' ] ' +
  274. '} ); ' +
  275. '}',
  276. ]);
  277. await renderMapContainsImports(renderMap, 'instructions/increment.ts', {
  278. '@solana/web3.js': ['Address', 'getProgramDerivedAddress'],
  279. });
  280. });
  281. test('it renders constants for instruction field discriminators', async () => {
  282. // Given the following instruction with a field discriminator.
  283. const node = programNode({
  284. instructions: [
  285. instructionNode({
  286. arguments: [
  287. instructionArgumentNode({
  288. defaultValue: numberValueNode(42),
  289. defaultValueStrategy: 'omitted',
  290. name: 'myDiscriminator',
  291. type: numberTypeNode('u64'),
  292. }),
  293. ],
  294. discriminators: [fieldDiscriminatorNode('myDiscriminator')],
  295. name: 'myInstruction',
  296. }),
  297. ],
  298. name: 'myProgram',
  299. publicKey: '1111',
  300. });
  301. // When we render it.
  302. const renderMap = visit(node, getRenderMapVisitor());
  303. // Then we expect the following constant and function to be rendered
  304. // And we expect the field default value to use that constant.
  305. await renderMapContains(renderMap, 'instructions/myInstruction.ts', [
  306. 'export const MY_INSTRUCTION_MY_DISCRIMINATOR = 42;',
  307. 'export function getMyInstructionMyDiscriminatorBytes() { return getU64Encoder().encode(MY_INSTRUCTION_MY_DISCRIMINATOR); }',
  308. '(value) => ({ ...value, myDiscriminator: MY_INSTRUCTION_MY_DISCRIMINATOR })',
  309. ]);
  310. });
  311. test('it renders constants for instruction constant discriminators', async () => {
  312. // Given the following instruction with two constant discriminators.
  313. const node = programNode({
  314. instructions: [
  315. instructionNode({
  316. discriminators: [
  317. constantDiscriminatorNode(constantValueNodeFromBytes('base16', '1111')),
  318. constantDiscriminatorNode(constantValueNodeFromBytes('base16', '2222'), 2),
  319. ],
  320. name: 'myInstruction',
  321. }),
  322. ],
  323. name: 'myProgram',
  324. publicKey: '1111',
  325. });
  326. // When we render it.
  327. const renderMap = visit(node, getRenderMapVisitor());
  328. // Then we expect the following constants and functions to be rendered.
  329. await renderMapContains(renderMap, 'instructions/myInstruction.ts', [
  330. 'export const MY_INSTRUCTION_DISCRIMINATOR = new Uint8Array([ 17, 17 ]);',
  331. 'export function getMyInstructionDiscriminatorBytes() { return getBytesEncoder().encode(MY_INSTRUCTION_DISCRIMINATOR); }',
  332. 'export const MY_INSTRUCTION_DISCRIMINATOR2 = new Uint8Array([ 34, 34 ]);',
  333. 'export function getMyInstructionDiscriminator2Bytes() { return getBytesEncoder().encode(MY_INSTRUCTION_DISCRIMINATOR2); }',
  334. ]);
  335. });
  336. test('it can override the import of a resolver value node', async () => {
  337. // Given the following node with a resolver value node.
  338. const node = programNode({
  339. instructions: [
  340. instructionNode({
  341. accounts: [
  342. instructionAccountNode({
  343. defaultValue: resolverValueNode('myResolver'),
  344. isSigner: false,
  345. isWritable: false,
  346. name: 'myAccount',
  347. }),
  348. ],
  349. name: 'myInstruction',
  350. }),
  351. ],
  352. name: 'myProgram',
  353. pdas: [pdaNode({ name: 'counter', seeds: [] })],
  354. publicKey: '1111',
  355. });
  356. // When we render it using a custom import.
  357. const renderMap = visit(
  358. node,
  359. getRenderMapVisitor({
  360. linkOverrides: {
  361. resolvers: { myResolver: 'someModule' },
  362. },
  363. }),
  364. );
  365. // Then we expect the resolver to be exported.
  366. await renderMapContains(renderMap, 'instructions/myInstruction.ts', ['myResolver(resolverScope)']);
  367. // And its import path to be overridden.
  368. await renderMapContainsImports(renderMap, 'instructions/myInstruction.ts', {
  369. someModule: ['myResolver'],
  370. });
  371. });
  372. test('it renders optional config that can override the program address', async () => {
  373. // Given the following instruction
  374. const node = programNode({
  375. instructions: [
  376. instructionNode({
  377. accounts: [],
  378. name: 'myInstruction',
  379. }),
  380. ],
  381. name: 'myProgram',
  382. publicKey: '1111',
  383. });
  384. // When we render it.
  385. const renderMap = visit(node, getRenderMapVisitor());
  386. // Then we expect an optional config parameter with an optional programAddress field
  387. // And we expect this to be used to override programAddress if it is set
  388. await renderMapContains(renderMap, 'instructions/myInstruction.ts', [
  389. 'config?: { programAddress?: TProgramAddress; }',
  390. 'programAddress = config?.programAddress ?? MY_PROGRAM_PROGRAM_ADDRESS',
  391. ]);
  392. });
  393. test('it renders instructions with no accounts and no data', async () => {
  394. // Given the following instruction with no accounts and no arguments.
  395. const node = programNode({
  396. instructions: [instructionNode({ name: 'myInstruction' })],
  397. name: 'myProgram',
  398. publicKey: '1111',
  399. });
  400. // When we render it.
  401. const renderMap = visit(node, getRenderMapVisitor());
  402. // Then the instruction input type is generated as an empty object.
  403. await renderMapContains(renderMap, 'instructions/myInstruction.ts', ['export type MyInstructionInput = {};']);
  404. // But the instruction function does not use it as an argument.
  405. await renderMapDoesNotContain(renderMap, 'instructions/myInstruction.ts', ['input: MyInstructionInput']);
  406. });
  407. test('it renders instructions with no accounts but with some omitted data', async () => {
  408. // Given the following instruction with no accounts but with a discriminator argument.
  409. const node = programNode({
  410. instructions: [
  411. instructionNode({
  412. arguments: [
  413. instructionArgumentNode({
  414. defaultValue: numberValueNode(42),
  415. defaultValueStrategy: 'omitted',
  416. name: 'myDiscriminator',
  417. type: numberTypeNode('u32'),
  418. }),
  419. ],
  420. discriminators: [fieldDiscriminatorNode('myDiscriminator')],
  421. name: 'myInstruction',
  422. }),
  423. ],
  424. name: 'myProgram',
  425. publicKey: '1111',
  426. });
  427. // When we render it.
  428. const renderMap = visit(node, getRenderMapVisitor());
  429. // Then the instruction input type is generated as an empty object.
  430. await renderMapContains(renderMap, 'instructions/myInstruction.ts', ['export type MyInstructionInput = {};']);
  431. // But the instruction function does not use it as an argument.
  432. await renderMapDoesNotContain(renderMap, 'instructions/myInstruction.ts', ['input: MyInstructionInput']);
  433. });
  434. test('it renders instructions with no accounts but with some arguments', async () => {
  435. // Given the following instruction with no accounts but with a non-omitted argument.
  436. const node = programNode({
  437. instructions: [
  438. instructionNode({
  439. arguments: [instructionArgumentNode({ name: 'myArgument', type: numberTypeNode('u32') })],
  440. name: 'myInstruction',
  441. }),
  442. ],
  443. name: 'myProgram',
  444. publicKey: '1111',
  445. });
  446. // When we render it.
  447. const renderMap = visit(node, getRenderMapVisitor());
  448. // Then we expect the following input type to be rendered
  449. // and used as an argument of the instruction function.
  450. await renderMapContains(renderMap, 'instructions/myInstruction.ts', [
  451. "export type MyInstructionInput = { myArgument: MyInstructionInstructionDataArgs['myArgument']; };",
  452. 'input: MyInstructionInput',
  453. ]);
  454. });
  455. test('it renders instructions with no arguments but with some accounts', async () => {
  456. // Given the following instruction with no arguments but with an account.
  457. const node = programNode({
  458. instructions: [
  459. instructionNode({
  460. accounts: [instructionAccountNode({ isSigner: false, isWritable: false, name: 'myAccount' })],
  461. name: 'myInstruction',
  462. }),
  463. ],
  464. name: 'myProgram',
  465. publicKey: '1111',
  466. });
  467. // When we render it.
  468. const renderMap = visit(node, getRenderMapVisitor());
  469. // Then we expect the following input type to be rendered
  470. // and used as an argument of the instruction function.
  471. await renderMapContains(renderMap, 'instructions/myInstruction.ts', [
  472. 'export type MyInstructionInput <TAccountMyAccount extends string = string> = { myAccount: Address<TAccountMyAccount>; };',
  473. 'input: MyInstructionInput<TAccountMyAccount>',
  474. ]);
  475. });